JavaScript-区块链编程学习手册-全-

JavaScript 区块链编程学习手册(全)

原文:zh.annas-archive.org/md5/FF38F4732E99A2380E8ADFA2F873CF99

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

借助本书,您将使用 JavaScript 编程语言构建自己的区块链原型和去中心化网络。构建自己的区块链将帮助您了解与区块链相关的各种概念,例如区块链技术在幕后是如何工作的,去中心化的区块链网络如何运作,以及如何使用 JavaScript 编写区块链和去中心化网络。此外,您将了解为什么区块链是如此安全和有价值的技术。

本书中构建的区块链将具有类似于比特币或以太坊等真实区块链上的功能,例如挖掘新区块的能力,创建新的不可变交易,并执行工作证明以保护区块链。除此之外,您的区块链还将包含许多其他重要功能。随着您进一步阅读各章节,您将有机会探索这些功能。

完成本书后,您将彻底了解区块链技术的实际运作方式,以及为什么这项技术如此安全和有价值。您还将深刻了解去中心化的区块链网络是如何运作的,以及为什么去中心化是保护区块链的重要特性。

本书适合对象

使用 JavaScript 学习区块链编程适用于希望学习区块链编程或使用 JavaScript 框架构建自己的区块链的 JavaScript 开发人员。

本书涵盖内容

第一章,设置项目,介绍了区块链的实际含义,并使读者了解其功能。然后,您将学习如何设置项目,以创建自己的区块链。

第二章,构建区块链,介绍了如何向您的区块链添加各种功能。您将在区块链中实现这些功能,创建一些令人惊叹的方法,如createNewBlockcreatNewTransactiongetLastBlock。一旦这些方法添加到区块链中,您将测试它们以验证其是否完美运行。此外,您还将了解哈希方法,即 SHA256 哈希,并实现一种方法来为您的区块数据生成哈希。此外,您还将了解工作证明是什么,它如何有益于区块链以及如何实现它。

第三章,通过 API 访问区块链,解释了如何在项目中设置 Express.js,以及如何使用它来构建 API/服务器。然后,您将为区块链构建各种服务器端点,并测试这些端点以验证它们是否正常工作。

第四章,创建去中心化的区块链网络,介绍了如何为您的区块链设置去中心化网络。在本章中,您将学习有关如何设置各种节点并将它们互连以形成网络的许多新概念。您还将定义各种端点,例如/register-and-broadcast-node/register-node/register-nodes-bulk。这些端点将帮助您实现去中心化的区块链网络。

第五章,同步网络,解释了如何同步整个去中心化的区块链网络,以便在区块链的所有节点上具有相同的交易数据和区块。您将通过重构端点来实现网络同步,将数据广播到网络中的所有节点。

第六章,共识算法,解释了如何构建自己的共识算法,该算法实现了最长链规则。通过实现这个算法,您将构建一个类似于现实生活中的区块链。

第七章,区块浏览器,解释了如何构建一个令人惊叹的用户界面,以便探索您在本书中构建的区块链。

第八章,总结,提供了本书学习过程中所学到的一切的快速总结。您还将探索如何改进您开发的区块链。

为了充分利用本书

建议具有基本的 JavaScript 知识。您还需要在系统上安装 Node.js。

本书中的示例代码和实现是在 macOS 上执行的。但是,如果您想要在 Windows PC 上实现所有这些,您将需要安装必要的要求。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learn-Blockchain-Programming-with-JavaScript。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富图书和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。这是一个例子:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"

代码块设置如下:

Blockchain.prototype.createNewBlock = function () { 

}

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目会以粗体显示:

Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 

}

任何命令行输入或输出都以以下方式编写:

cd dev
touch blockchain.js  test.js

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:"转到更多工具,然后选择开发者工具选项。"

警告或重要说明会以这种方式出现。

提示和技巧会以这种方式出现。

第一章:设置项目

欢迎来到使用 JavaScript 学习区块链编程。正如其名称所示,在本书中,您将学习如何使用 JavaScript 编程语言从头开始构建一个完全功能的区块链。您构建的区块链将具有类似于比特币或以太坊等生产级区块链中找到的功能。

在本书中,您将通过学习如何构建自己的区块链和理解分散网络来了解区块链技术的实际工作原理。在本书结束时,您将拥有一个托管在分散网络上的完整的区块链原型,并且您将对区块链在幕后实际工作的知识和理解有了很大的收获。

我们将在本书中创建的区块链将能够执行以下功能:

  • 执行工作证明以保护区块链

  • 通过挖矿过程创建新的区块

  • 创建新的不可变交易

  • 验证整个区块链以及每个区块内的所有数据

  • 检索地址/交易/区块数据

除此之外,区块链还将具有许多其他重要功能。随着您阅读本书的更多章节,您将有机会探索这些功能。

要跟随本书,您只需要一台计算机和一些关于 JavaScript 编程语言的基本知识。

首先,在本书的介绍章节中,让我们试着了解区块链实际上是什么。这将帮助您熟悉区块链的概念,因为这是本书的先决条件。然后我们将继续学习如何设置项目来创建我们自己的区块链。

所以,让我们开始吧!

什么是区块链?

在本节中,让我们简要解释一下什么是区块链。简而言之,区块链是一个不可变的、分布式分类帐。现在,这些词可能看起来很复杂,但当我们试图解释它们时,就会很容易理解。让我们从探索分类帐的实际含义开始。分类帐只是一组财务账户或交易(或者换句话说,人们进行的交易记录)。

让我们看下面的示例,以更好地理解分类帐。在这个例子中,Kim 支付给 Joe 30 美元,Kevin 支付给 Jen 80 美元。分类帐只是用来跟踪这些交易的文件。您可以在以下截图中看到这一点:

那么,区块链不可变意味着什么?这意味着它永远不能被改变。因此,一旦交易被记录,就无法撤销。其他无法更改的因素包括发送的金额或参与交易的人。一旦交易完成,该交易的任何方面都无法更改,因为它是不可变的。

今天,我们看到许多应用程序、平台和网络都是集中化的。以 Facebook 为例。使用 Facebook 的每个人都必须相信这家公司正在保护他们的数据并且不滥用它。与此相比,区块链是不同的。区块链技术不像 Facebook、Google 或大多数其他实体那样集中化。相反,它是一个分布式网络,这意味着任何给定的区块链网络都不受单一实体控制,而是由普通人运行。比特币等区块链由全球数千人支持和托管。因此,我们的所有数据,或者在这种情况下的分类帐,不受单一公司或实体的支配。这证明了区块链技术的巨大好处,因为通过分布式,我们不必信任单一公司来保护我们的数据。相反,我们的数据由成千上万个不同的人组成的整个网络持久保存。

每个为区块链网络做出贡献的个人都被称为节点,每个节点都有相同的分类账副本。因此,分类账数据在整个网络中进行托管和同步。

因此,区块链是一个不可变的分布式分类账。这意味着这是一个分类账,其中的交易永远不会被更改,区块链本身分布在网络中,并由成千上万的独立个人、团体或节点运行。

区块链是一种非常强大的技术,尽管它仍处于起步阶段,但它的未来非常令人兴奋。区块链技术可以应用于今天的世界,使某些行业更安全、高效和可信。一些可能通过区块链技术转变的行业包括金融服务、医疗保健、信用、政府、能源行业等。几乎每个行业都可以从更安全、分布式的数据管理形式中受益。您可以看到,区块链技术目前正处于一个非常令人兴奋的阶段,许多人对它的未来充满期待。

现在我们知道了什么是区块链,让我们开始设置项目环境来构建我们的区块链。

你将学到什么...

本书将通过从头开始构建自己的区块链来帮助您更深入地了解区块链技术。区块链是一种相当新的技术,虽然一开始学习起来可能会有些困难和有些压倒性,但我们将采取一步一步的方法,以便了解它在底层是如何工作的。当您完成本书时,您将对区块链技术的工作原理有很扎实的理解,并且您还将构建自己的整个区块链。

在本书中,我们将首先构建区块链本身。在这一点上,我们将构建一个具有以下能力的区块链数据结构:

  • 验证工作

  • 挖掘新区块

  • 创建交易

  • 验证链

  • 检索地址数据和其他功能

此后,我们将创建一个 API 或服务器,允许我们通过互联网与我们的区块链进行交互。通过我们的 API,我们将能够使用我们构建到区块链数据结构中的所有功能。

此外,您将学习创建一个去中心化网络。这意味着我们将有多个运行的服务器,作为独立的节点。我们还将确保所有节点之间正确地相互交互,并以正确的格式共享数据。此外,您将学习如何通过确保任何新创建的节点或交易都在整个网络中广播来同步整个网络。

接下来,我们将开始创建共识算法。该算法将用于确保我们整个区块链保持同步,并且该算法将用于确保我们网络中的每个节点都具有正确的区块链数据。

最后,我们将创建一个区块浏览器。这将是一个用户界面,允许我们以用户友好的方式探索我们的区块链,还将允许我们查询特定的区块交易和地址。

然而,首先,我们需要设置我们的开发环境。

环境设置

让我们开始构建我们的区块链项目。我们要做的第一件事是打开我们的终端,并通过在终端中输入命令来创建我们的区块链目录,如下面的屏幕截图所示:

让我们首先创建一个名为programs的文件夹。在这个文件夹里,让我们创建一个名为blockchain的目录。这个目录目前是空的。在这个blockchain目录里,我们将进行所有的编程。我们将在这个blockchain目录中构建我们的整个区块链。

现在我们的blockchain目录已经准备好了,我们需要做的第一件事是向其中添加一些文件夹和文件。我们想要放入目录的第一个文件夹将被称为dev,因此我们要确保我们在blockchain目录中,然后让我们在终端中输入以下命令:

mkdir dev

在这个dev目录中,我们将进行大部分编码工作。这是我们将构建区块链数据结构并创建与区块链交互的 API、测试它以及完成其他类似任务的地方。接下来,在这个dev文件夹中,让我们创建两个文件:blockchain.jstest.js。为此,请在终端中输入以下命令:

cd dev
touch blockchain.js test.js

在上述命令行中的touch命令将帮助我们创建提到的文件。blockchain.js文件是我们将输入代码以创建区块链的地方,test.js文件是我们将编写代码来测试我们的区块链的地方。

接下来,让我们通过在终端中输入以下命令返回到我们的blockchain目录:

cd .. 

blockchain目录中,让我们运行以下命令来创建 npm 项目:

npm init 

运行上述命令后,您将在终端上获得一些选项。要设置项目,您只需通过这些选项按Enter即可。

因此,这基本上是我们需要做的一切,以便设置我们的项目文件夹结构。现在,如果您转到我们的blockchain目录并使用 Sublime 或 Atom(或您喜欢的任何其他文本编辑器)打开它,您将看到文件结构,如下截图所示:

blockchain目录包括我们刚刚创建的dev文件夹。在dev文件夹中,我们可以看到我们的blockchain.jstest.js文件。此外,当我们运行npm init命令时,它会为我们创建package.json文件。这个.json文件将跟踪我们的项目和我们需要的任何依赖项,使我们能够运行脚本。在后续章节中,我们将在package.json文件中进行更多工作,因此随着我们在本书中的进展,您将更加熟悉它。

项目源代码

在我们开始编写区块链之前,值得注意的是,本书的整个源代码可以在 GitHub 上找到,链接如下:github.com/PacktPublishing/Learn-Blockchain-Programming-with-JavaScript。在这个存储库中,您将找到整个项目的完成代码,并且您还将能够探索我们将在后续章节中构建的所有文件。因此,这可能是您在阅读本书时使用的一个很好的资源。

摘要

总结一下这个介绍性的章节,我们首先探讨了区块链的实际含义以及它的运作方式。然后我们开始设置项目以创建我们自己的区块链。我们还快速概述了本书中您将学习的所有主题。

在下一章中,我们将通过学习构造函数、原型对象、区块方法、交易方法以及许多其他重要概念来构建我们的区块链。

第二章:构建区块链

在上一章中,我们了解了区块链是什么以及它的功能。此外,我们还学习了如何设置项目来构建我们的区块链。在本章中,您将开始构建区块链及其所有功能。首先,让我们使用构造函数创建区块链数据结构,然后通过向其原型添加不同类型的功能来为我们的区块链添加许多不同类型的功能。

然后,我们将赋予区块链某些功能,例如创建新的区块和交易,以及对数据和区块进行哈希的能力。我们还将赋予它进行工作证明和许多其他区块链应该具备的功能。然后,我们将通过测试添加的功能来确保区块链是完全功能的。

通过逐步构建区块链的每个部分,您将更好地了解区块链在幕后实际上是如何工作的。您还可能意识到,一旦您深入其中,创建区块链并不像听起来那么复杂。

在本章中,我们将涵盖以下主题:

  • 学习如何创建区块链构造函数

  • 构建和测试各种方法,如createNewBlockcreateNewTransactionhashBlock,以为区块链添加功能

  • 了解工作证明是什么,并学习如何为我们的区块链实现它

  • 创建和测试创世区块

所以,让我们开始吧!

在我们开始之前...

在构建区块链之前,有两个关键概念我们需要熟悉。这些重要概念如下:

  • JavaScript 构造函数

  • 原型对象

JavaScript 构造函数的解释

熟悉构造函数很重要,因为我们将使用它来构建我们的区块链数据结构。到目前为止,您一定想知道构造函数是什么,它实际上是做什么。

构造函数只是一个创建对象类并允许您轻松创建该特定类的多个实例的函数。这实际上意味着构造函数允许您非常快速地创建大量对象。由于它们都是同一类的一部分,所以创建的所有这些对象都将具有相同的属性和功能。现在,当您第一次听到这些时,所有这些可能看起来有点令人困惑,但不要担心——我们将尝试通过一个示例来理解构造函数是什么。

以 Facebook 为例。Facebook 拥有超过 15 亿用户,它们都是同一类的对象,并具有类似的属性,如姓名、电子邮件、密码、生日等。对于我们的示例,假设我们正在构建 Facebook 网站,并希望为其创建一堆不同的用户。我们可以通过创建一个User构造函数来实现这一点。

要学习和探索构造函数,让我们使用 Google Chrome 控制台。我们可以通过打开 Google Chrome 并简单地按下command + option + J(Mac 用户)或Ctrl + Shift + I(Windows 用户)来访问控制台。或者,我们可以简单地转到菜单选项,转到“更多工具”,然后选择“开发者工具”选项,如下截图所示:

按照上述步骤将为您打开控制台,如下截图所示:

在本示例中,我们将编写的构造函数将允许我们创建多个用户或多个用户对象,这些对象将具有相同的属性和功能。创建此User构造函数的代码如下所示:

function User() { 

}

在括号()内,让我们传递我们希望每个User对象具有的属性。我们将传递诸如firstNamelastNameagegender等属性,因为我们希望所有的用户对象都具有这些组件。

然后,我们使用this关键字将这些参数分配给我们的User对象,如下面的代码块所示:

这就是我们在 JavaScript 中定义构造函数的方法。现在,通过阅读上面的代码块,你可能会想知道我们做了什么,this关键字是什么意思。

我们将使用这个构造函数来创建很多用户对象。this关键字只是简单地指向我们将要创建的每一个用户对象。现在可能看起来有点令人不知所措,但让我们通过一些例子来更清楚地理解它。

让我们开始使用我们的User构造函数。要创建一些User对象,也称为User实例,请按照以下步骤进行:

  1. 我们要创建的第一个用户 - 让我们称之为user1 - 将被定义如下:
var user1 = new User('John','Smith',26,'male');

在上面的代码中,你可能已经注意到我们使用了new关键字来调用我们的构造函数并创建一个用户对象,这就是我们让构造函数工作的方法。

  1. 然后按下Enteruser1就出现在系统中。现在,如果我们在控制台中输入user1,我们将能够看到我们在上一步中创建的内容:

在上面的输出截图中,我们可以看到user1User类的一个对象。我们还可以看到user1firstNameJohnlastNameSmithage26gendermale,因为这些是我们传入构造函数的参数。

  1. 为了更清晰,尝试添加一个用户。这一次,我们将创建另一个名为user200的用户,并将其传递到new User()函数中,传入用户的属性,例如名字为Jill,姓氏为Robinson,年龄为25,性别为female
var user200 = new User('Jill', 'Robinson', 25, 'female');
  1. 按下Enter,我们的新user200将出现在系统中。现在,如果我们在控制台中输入user200并按下Enter,我们将看到以下输出:

在上面的输出中,我们可以看到user200User类的一个对象,就像user1一样,她的名字是Jill,姓氏是Robinson,年龄是25,性别是female,因为这些是我们传入构造函数的参数。

现在,你可能想知道我们提到的所有这些属性是如何被正确分配的。这都是由我们之前提到的this关键字所致。当我们创建我们的构造函数时,我们使用this关键字来分配属性。当涉及到构造函数时,this关键字不是指代它所在的函数 - 在我们的例子中是User函数。相反,this指的是将由构造函数创建的对象。

这意味着,如果我们使用构造函数来创建一个对象,我们必须确保属性和它们的对象是名字、姓氏、年龄和性别,或者无论何时你创建你的构造函数,都要将firstName属性设置为等于传入的firstName参数,并对其余属性做同样的操作。

这就是构造函数的工作原理,以及this关键字在构造函数中扮演的重要角色。

原型对象的解释

在编写区块链数据结构之前,我们需要讨论的另一个重要概念是原型对象。原型对象只是一个多个其他对象可以引用以获取它们需要的任何信息或功能的对象。对于我们在上一节中讨论的示例,我们的每个构造函数都将有一个原型,它们的所有实例都可以引用。让我们通过探索一些例子来尝试理解原型对象的含义。

例如,如果我们拿出我们在上一节中创建的User构造函数,我们可以将这些属性放在它的原型上。然后,我们所有的用户实例,如user1user200,都将可以访问并使用该原型。让我们在User原型上添加一个属性并看看会发生什么。要在用户原型上添加一个属性,我们将输入以下代码:

User.prototype. 

然后让我们在上面的代码中添加属性的名称。例如,假设我们想要一个属性电子邮件域:

User.prototype.emailDomain 

对于我们的示例,假设 Facebook 希望每个用户都有一个@facebook.com的电子邮件地址,因此我们将设置电子邮件域属性如下:

User.prototype.emailDomain = '@facebook.com';

现在让我们再次检查我们的user1对象:

在上面的截图中,我们可以看到user1没有我们刚刚添加的电子邮件域属性。但是,我们可以展开user1对象以及它的 dunder proto,如下面的截图所示:

当我们这样做时,我们可以观察到我们刚刚添加的emailDomain属性,它被设置为@facebook.com

只是为了澄清,dunder proto 和我们实际放置emailDomain属性的原型对象实际上并不完全相同,但非常相似。基本上,我们放在构造函数原型上的任何东西都可以访问我们使用构造函数创建的任何对象的 dunder proto。

因此,如果我们在构造函数原型上放置emailDomain,我们将可以在user1 dunder proto、user200 dunder proto 以及我们创建的任何其他用户实例的 dunder proto 上访问它。

现在让我们回到emailDomain属性。我们将emailDomain属性放在用户原型上。我们可以看到我们在实际的user200对象上没有该属性,但是我们在user200的 dunder proto 下有该属性。因此,如果我们输入以下命令,我们仍然可以访问该属性:

user200.emailDomain

然后我们应该看到以下输出:

因此,这就是原型对象的工作原理。如果我们在构造函数的原型上放置一个属性,那么构造函数的所有实例都将可以访问该属性。

对于我们可能希望所有实例都具有的任何方法或函数,都适用相同的情况。让我们看另一个例子,假设我们希望所有用户实例都有一个getEmailAddress方法。我们可以将其放在构造函数的原型上,如下所示:

User.prototype.getEmailAddress = function () { 
}    

现在让我们让这个getEmailAddress方法返回一些特定的属性,如下所示(高亮显示):

User.prototype.getEmailAddress = function () { 
 return this.firstName + this.lastName + this.emailDomain;
} 

现在user1user200都应该在它们的 dunder proto 下有这个方法,所以让我们来检查一下。在我们的用户下输入,并在它们的 dunder proto 下你将看到前面的函数,如下面的截图所示:

在上面的截图中,我们可以观察到user1user200都在它们的 dunder proto 下有getEmailAddress方法。

现在,如果我们输入user200.getEmailAddress然后调用它,该方法将为我们创建 user200 的 Facebook 电子邮件地址,如下面的截图所示:

如果我们为user1调用该方法,类似的事情也会发生:

这就是我们如何使用原型对象与构造函数。如果我们希望我们的构造函数实例都具有相同的属性,或者都具有相同的方法,我们将把它放在原型上,而不是构造函数本身。这将有助于保持实例更加精简和清晰。

这是我们需要了解的所有背景信息,以便开始编写我们的区块链数据结构。在接下来的部分中,我们将通过使用构造函数和原型对象来开始构建我们的区块链。

区块链构造函数

让我们开始构建我们的区块链数据结构。我们将首先通过使用 Sublime 编辑器打开我们区块链目录中的所有文件。如果你习惯使用其他编辑器,也可以使用。在你喜欢的任何编辑器中打开我们整个区块链目录。

我们将在我们在第一章中创建的dev/blockchain.js文件中构建整个区块链数据结构,设置项目。让我们通过使用我们在上一节中学到的构造函数来构建这个区块链数据结构。所以,让我们开始:

对于构造函数,请键入以下内容:

function Blockchain () {
}

目前,Blockchain()函数不会接受任何参数。

接下来,在我们的构造函数内部,我们将添加以下术语:

function Blockchain () {
    this.chain = [];
    this.newTransactions = [];
}

在上面的代码块中,[]定义了一个数组,而this.chain = [];是我们的区块链的核心所在。我们挖掘的所有区块都将存储在这个特定的数组中作为一个链,而this.newTransactions = [];是我们将在放入区块之前创建的所有新交易的存储位置。

现在,所有这些可能看起来有点混乱和令人不知所措,但不用担心。让我们在未来的部分深入了解这一点。

在定义上述函数时,我们已经开始了创建区块链数据结构的过程。现在,你可能会想为什么我们要使用构造函数来构建我们的区块链数据结构,而不是类;答案是这只是一种偏好。在 JavaScript 中,我们更喜欢使用构造函数而不是类,因为在 JavaScript 中实际上并没有类。JavaScript 中的类只是构造函数和对象原型的一种糖衣。所以,我们更喜欢坚持使用构造函数。

但是,如果你想使用类来创建区块链,你可以像下面的代码块一样做:

class Blockchain {
    constructor() {
        this.chain = [];
        this.newTransactions = [];
    }

    // Here you can build out all of the methods 
    // that we are going to write inside of this
    // Blockchain class. 

}

所以,无论你喜欢使用构造函数还是类,都可以正常工作。

就是这样 - 通过定义我们的函数,我们已经开始了构建我们的区块链数据结构的过程。在后续部分中,我们将继续构建。

构建createNewBlock方法

让我们继续构建我们的区块链数据结构。在上一节中定义了我们的构造函数之后,我们想要做的下一件事是在我们的Blockchain函数中放置一个方法。我们将要创建的这个方法将被称为createNewBlock。顾名思义,这个方法将为我们创建一个新的区块。让我们按照下面提到的步骤来构建这个方法:

  1. createNewBlock方法将定义如下:
Blockchain.prototype.createNewBlock = function () { 

}
  1. 现在我们在我们的区块链prototype对象上有了这个createNewBlock方法。这个方法将使用下面代码行中突出显示的三个参数:
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 

}

我们将在后面的部分深入学习这三个参数,所以如果你对它们不熟悉,不用担心。

  1. 现在,我们在createNewBlock方法内想要做的下一件事是创建一个newBlock对象。让我们定义如下:
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 

 }; 

}

这个newBlock对象将成为我们BlockChain中的一个新区块,因此所有数据都将存储在这个区块中。这个newBlock对象是我们区块链的一个非常重要的部分。

  1. 接下来,在newBlock对象上,我们将有一个index属性。这个index值基本上就是区块编号。它将描述newBlock在我们的链中的区块编号(例如,它可能是第一个区块):
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,     
    };   

}
  1. 我们接下来的属性将是一个timestamp,因为我们想知道区块是什么时候创建的:
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),       
    };   

}
  1. 接下来,我们要添加的属性是transactions。当我们创建一个新区块时,我们将希望将所有新的交易或者刚刚创建的待处理交易放入新区块中,以便它们在我们的区块链中,并且永远不会被更改:
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),
        transactions: this.newTransactions,          
    };   

}

前面突出显示的代码行表示区块中的所有交易应该是等待放入区块中的新交易。

  1. 我们区块的下一个属性将是一个nonce,它将等于我们之前传递到函数中的nonce参数:
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),
        transactions: this.newTransactions, 
        nonce: nonce,         
    };   

}

现在,你可能想知道nonce是什么。基本上,nonce 来自于工作证明。在我们的情况下,这只是一个数字;它无关紧要。这个 nonce 基本上证明了我们通过使用proofOfWork方法以合法的方式创建了这个新区块。

现在所有这些可能看起来有点混乱,但不要担心——一旦我们在区块链数据结构上建立更多内容,就会更容易理解所有东西是如何一起工作的,从而创建一个功能性的区块链。所以,如果你现在不理解 nonce 是什么,不要担心。我们将在后续章节中处理这个属性,随着我们的进展,它会变得更清晰。

  1. 接下来的属性将是一个hash
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),
        transactions: this.newTransactions, 
        nonce: nonce,
        hash: hash,         
    };   

}

基本上,这个hash将是我们newBlock的数据。发生的情况是我们将我们的交易或者newTransactions传递到一个哈希函数中。这意味着我们所有的交易将被压缩成一个代码字符串,这将是我们的hash

  1. 最后,我们newBlock上的最后一个属性将是我们的previousBlockHash
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),
        transactions: this.newTransactions, 
        nonce: nonce,
        hash: hash,
        previousBlockHash: previousBlockHash,          
    };   

}

这个previousBlockHash属性与我们的hash属性非常相似,只是我们的hash属性处理的是我们当前区块的数据哈希成一个字符串,而previousBlockHash属性处理的是我们上一个区块或者当前区块的上一个区块的数据哈希成一个字符串。

因此,hashpreviousBlockHash都是哈希。唯一的区别是hash属性处理的是当前区块的数据,而previousBlockHash属性处理的是上一个区块的数据的哈希。这就是如何创建一个新区块,这就是我们区块链中每个区块的样子。

  1. 继续我们的createNewBlock方法,我们接下来要做的是将this.newTransaction设置为空数组,如下所示:
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),
        transactions: this.newTransactions, 
        nonce: nonce,
        hash: hash,
        previousBlockHash: previousBlockHash,          
    };

    this.newTransaction = [];  

}

我们这样做是因为,一旦我们创建了新的区块,我们就将所有新的交易放入newBlock中。因此,我们希望清空整个新交易数组,以便我们可以为下一个区块重新开始。

  1. 接下来,我们要做的就是将我们创建的新区块推入我们的链中,然后我们将返回newBlock
Blockchain.prototype.createNewBlock = function (nonce, previousBlockHash, hash) { 
    const newBlock = { 
        index: this.chain.length + 1,
        timestamp: Date.now(),
        transactions: this.newTransaction, 
        nonce: nonce,
        hash: hash,
        previousBlockHash: previousBlockHash,          
    };

    this.newTransaction = [];
    this.chain.push(newBlock);    

    return newBlock; 
}

通过添加这最后两行代码,我们的createNewBlock方法已经准备好了。基本上,这个方法在高层次上所做的就是创建一个新区块。在这个区块内,我们有我们的交易和自上一个区块被挖掘以来创建的新交易。创建了新区块后,让我们清空新交易,将新区块推入我们的链中,然后简单地返回我们的新区块。

测试createNewBlock方法

现在让我们测试我们在前面部分创建的createNewBlock方法:

  1. 我们需要做的第一件事是导出我们的Blockchain构造函数,因为我们将在我们的test.js文件中使用这个函数。因此,为了导出构造函数,我们将转到blockchain.js文件的底部,输入以下代码行,然后保存文件:
module.exports = Blockchain;
  1. 接下来,转到dev/test.js文件,因为这是我们将测试createNewBlock方法的地方。现在,在dev/test.js文件中,我们要做的第一件事是导入我们的Blockchain构造函数,因此输入以下内容:
const Blockchain = require('./blockchain');

上述代码行只是需要或调用blockchain.js文件。

测试 Blockchain 构造函数

让我们按照以下方式测试 Blockchain 构造函数:

  1. 让我们创建一个Blockchain构造函数的实例,因此我们将添加以下代码行:
const bitcoin = new Blockchain();
  1. 上一行代码中的bitcoin变量只是用作示例。然后我们添加以下代码行:
console.log(bitcoin); 

通过上述代码行,bitcoin应该是我们的区块链。目前这里没有数据或区块,但它应该作为一个区块链记录出来。让我们保存test.js文件并运行测试,观察终端窗口上的输出。

  1. 现在转到我们的终端窗口。在这里,我们目前在blockchain目录中,我们的test.js文件在我们的dev文件夹中,因此在终端中输入以下命令:
node dev/test.js

上述代码行将允许我们运行我们编写的测试来测试我们的Blockchain构造函数。

  1. 现在按下Enter,我们将在终端窗口上观察Blockchain,如下截图所示:

从上述截图的输出中,我们可以观察到Blockchain有一个空的链和一个空的交易数组。这正是我们预期的输出。

测试 createNewBlock 方法

让我们按照以下步骤测试 createNewBlock 方法:

  1. 首先,在我们创建bitcoin变量的地方下面,输入以下突出显示的代码行:
const Blockchain = require('./blockchain');

const bitcoin = new Blockchain();

bitcoin.createNewBlock();

console.log(bitcoin); 
  1. 这个createNewBlock()方法需要三个参数,比如noncepreviousBlockHashhash。为了测试目的,我们现在可以随便传入一些值。这里,nonce 只是一个数字。然后我们将为我们的previousBlockHash创建一个虚拟哈希,然后为我们的hash参数创建另一个哈希,如下所示:
bitcoin.createNewBlock(2389,'OIUOEREDHKHKD','78s97d4x6dsf');

现在,我们正在创建我们的bitcoin区块链,然后在我们的比特币区块链中创建一个新区块。当我们退出比特币区块链时,我们应该有一个区块。

  1. 保存此文件并在终端中再次运行我们的test.js文件。然后您将观察到以下输出:

在上述截图中,您可以观察到chain数组中的整个区块链数据结构。它里面有一个区块,或者说一个对象。这个区块还有我们传递的hashnoncepreviousBlockHash参数。它还有timestampindex1。它没有交易,因为我们还没有创建任何交易。因此,我们可以得出结论,createNewBlock方法运行正常。

  1. 现在让我们通过在我们的链中创建更多的区块来进一步测试我们的方法。让我们多次复制以下代码行,然后尝试更改其中的值:
bitcoin.createNewBlock(2389,'OIUOEREDHKHKD','78s97d4x6dsf');
  1. 复制代码并更改值后保存文件。现在,当我们运行test.js文件时,我们应该在我们的链中有三个区块,如下截图所示:

在上述截图中,您可能已经观察到chain数组中的三个区块。这些都是我们用createNewBlock方法创建的所有区块。

构建 getLastBlock 方法

现在,我们要添加到我们的Blockchain构造函数中的下一个方法将是getLastBlock。这个方法将简单地返回我们区块链中的最后一个块。按照下面提到的步骤来构建这个方法:

  1. 转到我们的dev/blockchain.js文件,在我们的createNewBlock方法之后添加以下内容:
Blockchain.prototype.getLastBlock = function () { 

}
  1. getLastBlock方法中,我们将输入以下突出显示的代码行:
Blockchain.prototype.getLastBlock = function () { 
    return this.chain[this.chain.length - 1];

}

在上述代码中的[this.chain.length - 1];定义了链中块的位置,在我们的情况下是前一个块,因此通过1进行否定。这个方法简单明了,我们将在后面的章节中使用它。

创建createNewTransaction方法

我们要添加到我们的区块链构造函数中的下一个方法是createNewTransaction。这个方法将为我们创建一个新的交易。让我们按照下面提到的步骤来创建这个方法:

  1. 通过在我们的getLastBlock方法之后添加以下代码来开始构建这个方法:
Blockchain.prototype.createNewTransaction = function () {

}
  1. function ()将接收三个参数,如下所示:
Blockchain.prototype.createNewTransaction = function (amount, sender, recipient) {

}

这三个参数的作用如下:

  • amount:此参数将接收交易金额或此交易发送的金额。

  • sender:这将接收发件人的地址。

  • recipient:这将接收收件人的地址。

  1. 我们在createNewTransaction方法中要做的下一件事是创建一个交易对象。因此,将以下代码添加到我们的方法中:
const newTransaction = {

}
  1. 这个对象将有三个属性。它将有一个amount,一个sender和一个recipient。这些都是我们传递给function()的相同三个参数。因此,输入以下内容:
Blockchain.prototype.createNewTransaction = function (amount, sender, recipient) {
    const newTransaction = {
        amount: amount,
 sender: sender,
 recipient: recipient,
    };

}

这就是我们的交易对象将会是什么样子。我们在Blockchain上记录的所有交易都将看起来像这样。它们都将有一个金额,一个发件人和一个收件人,这非常简单明了。

  1. 我们现在要做的下一件事是将这个newTransaction数据推送到我们的newTransactions数组中。让我们在newTransaction对象之后添加以下代码来实现这一点:
this.newTransactions.push(newTransaction);

因此,我们刚刚创建的新交易现在将被推送到我们的newTransactions数组中。

现在,让我们试着理解一下这个newTransactions数组实际上是什么。基本上,这个newTransactions数组在我们的区块链上会有很多人进行很多不同的交易。他们将会把钱从一个人发送到另一个人,这将会重复发生。每当创建一个新交易时,它都会被推送到我们的newTransactions数组中。

然而,这个数组中的所有交易实际上并没有被确定下来。它们实际上还没有被记录在我们的区块链上。当挖掘新块时,也就是创建新块时,所有这些新交易基本上只是待处理交易,并且尚未被验证。当我们使用createNewBlock方法创建新块时,它们将被验证,确定下来,并记录在我们的区块链上。

在我们的createNewBlock方法中,您可以观察到在transactions: this.newTransactions中,我们将新块上的交易设置为newTransactions或我们区块链中的待处理交易。您可以将我们区块链上的newTransactions属性视为待处理交易属性。

为了方便参考,让我们实际上将代码中的所有newTransactions属性更改为pendingTransactions属性。总的来说,当创建新交易时,它被推送到我们的pendingTransactions数组中。然后,当挖掘新块或创建新块时,我们的所有待处理交易都会记录在我们的区块链上,然后它们就被确定下来,永远不能被更改。

所有这一切的重点是,在我们的方法结束之前,我们希望返回我们将能够找到新交易的区块,因为当新交易被挖掘时,我们的新交易将在下一个区块中。因此,我们只需输入以下代码:

this.newTransactions.push(newTransaction);
return.this.getlastBlock()['index'] + 1;

在上述代码中,this.getlastBlock()为我们返回一个区块对象。我们想要获取这个区块的 index 属性 - 添加['index']将为我们提供链中最后一个区块的索引,添加+ 1将为我们提供我们的交易被推送到的区块的编号。

让我们快速回顾一下,createNewTransaction方法只是创建一个newTransaction对象,然后我们将newTransaction推送到我们的pendingTransactions数组中。最后,我们返回newTransaction将被添加到的区块的编号。

测试 createNewTransaction 方法

让我们测试在上一节中创建的createNewTransaction方法。提醒一下:这一节将会非常有趣,因为在这里你将真正开始理解区块链有多强大,以及区块和交易是如何相互作用的。您还将学习交易如何记录在区块链中。所以让我们开始吧:

  1. 我们将在我们的test.js文件中测试我们的createNewTransaction方法。在这个文件中,我们已经需要了我们的blockchain.js文件,并创建了一个名为bitcoinBlockchain的新实例,我们在文件末尾记录了它。快速查看以下截图:

  1. 现在,在我们的test.js文件中,我们要做的第一件事是使用我们的createNewBlock方法创建一个新的区块,类似于我们在测试 createNewBlock 方法部分所做的。在您的test.js文件中输入以下内容:
bitcoin.createNewBlock(789457,'OIUOEDJETH8754DHKD','78SHNEG45DER56');
  1. 接下来,我们要做的是创建一些新的交易来测试我们的createNewTransaction方法。这个createNewTransaction方法接受三个参数,比如amountsenderrecipient。让我们将这个交易数据添加到我们的测试用例中:
bitcoin.createNewTransaction(100,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');

在上述代码行中,我们将交易金额设置为100,发送方和接收方的地址设置为一些随机哈希数。

您可能已经注意到地址中的ALEXJEN的名称。我们添加这些只是为了简化发送方和接收方的识别。实际上,您很可能不会在地址开头看到这种名称。我们这样做是为了更容易地引用这些地址。

现在,让我们快速总结一下我们在测试用例中到目前为止所做的事情。看一下以下代码块:

const Blockchain = require('./blockchain');

const bitcoin = new Blockchain();

bitcoin.createNewBlock(789457,'OIUOEDJETH8754DHKD','78SHNEG45DER56');

bitcoin.createNewTransaction(100,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');

console.log(bitcoin); 

在上述代码中,我们首先需要了比特币区块链,然后创建了一个新的区块。之后,我们创建了一个新的交易,然后记录了比特币区块链。

当我们运行这个test.js文件时,我们应该期望看到我们的比特币区块链,它应该有一个链中的区块以及pendingTransactions数组中的一个交易,因为我们在创建交易后还没有挖掘或创建新的区块。让我们保存这个文件并运行它看看我们得到什么。

  1. 现在转到您的终端窗口,输入以下命令,然后按Enter
node dev/test.js 

我们可以在终端窗口上观察比特币区块链,如下截图所示:

在您的窗口输出和上述截图中,您可以观察到我们的链,其中有我们创建的一个区块。在我们的pendingTransactions数组中,我们有一个待处理的交易,这是我们在测试用例中创建的交易。从测试的输出来看,我们可以得出结论,到目前为止,我们的createNewTransaction方法运行正常。

向我们的区块链添加待处理交易

现在让我们试着理解如何将pendingTransaction放入我们实际的chain中。我们这样做的方式是通过挖掘一个新的区块或创建一个新的区块。现在就让我们这样做:

  1. 在创建newTransaction之后,让我们使用createNewBlock方法创建一个新的区块,如下面的代码所示:
const Blockchain = require('./blockchain');

const bitcoin = new Blockchain();

bitcoin.createNewBlock(789457,'OIUOEDJETH8754DHKD','78SHNEG45DER56');

bitcoin.createNewTransaction(100,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');

bitcoin.createNewBlock(548764,'AKMC875E6S1RS9','WPLS214R7T6SJ3G2');

console.log(bitcoin);

我们所做的是创建一个区块,创建一个交易,然后挖掘一个新的区块。现在我们创建的交易应该出现在我们的第二个区块中,因为我们在创建交易后挖掘了一个区块。

  1. 现在保存文件并再次运行测试。让我们看看从中得到了什么。去你的终端,再次输入node dev/test.js命令并按Enter。你将看到以下截图中显示的输出:

在这里,我们再次拥有了我们的整个区块链,其中有两个区块,因为我们挖掘了两个区块。这个链有我们的第一个区块(索引:1),其中没有交易,还有我们的第二个区块(索引:2),在其中,如果你看我们的交易,它说有一个包含项目的数组,而第一个区块的交易数组中没有项目。

  1. 现在仔细看看第二个区块的交易数组。我们应该期望看到我们之前创建的交易。让我们对我们的测试案例进行以下突出显示的修改:
const Blockchain = require('./blockchain');

const bitcoin = new Blockchain();

bitcoin.createNewBlock(789457,'OIUOEDJETH8754DHKD','78SHNEG45DER56');

bitcoin.createNewTransaction(100,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');

bitcoin.createNewBlock(548764,'AKMC875E6S1RS9','WPLS214R7T6SJ3G2');

console.log(bitcoin.chain[1]);
  1. 在这个修改中,我们只是登出了我们链中的第二个区块。代码中的[1]定义了第二个区块的位置。保存这个文件并运行它。在输出中,你可以观察到我们只是登出了我们链中的第二个区块,并且你可以看到,对于交易,它有一个包含一个对象的数组。查看以下截图:

这个对象是我们在测试中创建的交易。我们在这里所做的就是创建一个交易,然后通过创建一个新的区块或挖掘一个新的区块来挖掘它,现在我们的交易就在其中了。

现在,让我们进行几个更多的示例,以帮助澄清这里发生了什么。让我们在createNewBlock方法之后再次复制并粘贴createNewTransaction方法三次。根据需要修改金额。

这里发生的情况是,从顶部开始,我们首先创建一个区块,然后创建一个交易。然后我们创建或挖掘一个新的区块,所以我们应该有一个没有交易的区块和另一个有一个交易的区块。在创建第二个区块后,我们创建了另外三个新的交易。此时,这三个新的交易应该都在我们的pendingTransactions数组中,因为我们在创建这三个交易后没有创建新的区块。最后,我们再次登出我们的比特币区块链。你的测试现在应该类似于以下内容:

const Blockchain = require('./blockchain');

const bitcoin = new Blockchain();

bitcoin.createNewBlock(789457,'OIUOEDJETH8754DHKD','78SHNEG45DER56');

bitcoin.createNewTransaction(100,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');

bitcoin.createNewBlock(548764,'AKMC875E6S1RS9','WPLS214R7T6SJ3G2');

bitcoin.createNewTransaction(50,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');
bitcoin.createNewTransaction(200,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');
bitcoin.createNewTransaction(300,'ALEXHT845SJ5TKCJ2','JENN5BG5DF6HT8NG9');

console.log(bitcoin);

现在,如果我们保存文件并运行它,我们应该在我们的链中有两个区块,并且在pendingTransactions数组中也应该有三个交易。让我们看看我们在这里得到了什么。你将在屏幕上看到以下输出:

在上面的截图中,你可以看到我们的区块链。在这个链中,我们有两个区块,就像我们期望的那样,并且在我们的pendingTransactions数组中,我们有三个交易,这就是我们在测试文件中创建的三个交易。

接下来我们要做的是将这些待处理的交易放入我们的链中。为此,让我们挖掘另一个区块。只需在我们创建的三个交易后复制并粘贴creatNewBlock方法,并根据需要修改其参数。当我们现在运行测试时,这三个待处理的交易应该出现在我们的新区块中。保存文件并运行测试。你将看到以下输出:

所以,我们有我们的区块链,其中有三个区块。我们的pendingTransactions数组目前是空的,但是那三笔交易去哪了呢?事实证明,它们应该在我们创建的最后一个区块中,也就是索引:3 区块。在这第三个区块中,我们有我们的交易,应该是我们刚刚创建的三笔交易。让我们通过对我们测试代码的最后一行进行微小修改来更深入地了解一下,即console.log(bitcoin.chain[2]);。这里的值2指定了链中的第三个区块。让我们保存这个修改并再次运行测试。你将看到链中的第三个区块:

在交易数组中,你可以看到我们有我们创建的所有三个交易。这就是我们的createNewTransactioncreateNewBlock方法是如何一起工作的。

如果你对这两种方法如何工作或它们如何一起工作有困难,我们鼓励你在test.js文件中进行一些实验,创建一些新的区块,创建一些新的交易,记录一些不同的信息,并对这些事情如何工作有一个很好的理解。

对数据进行哈希处理

我们将要看的下一个方法并添加到我们的区块链数据结构中的是hashBlock。这个hashBlock方法将接收我们的区块并将其数据哈希成一个固定长度的字符串。这个哈希数据将会是随机的。

实质上,我们将把一些数据块传递到这个哈希方法中,作为返回我们将得到一个固定长度的字符串,这个字符串将简单地是从我们传入的数据或我们传入的区块生成的哈希数据。

要将hashBlock方法添加到我们的区块链数据结构中,请在我们的createNewTransaction方法之后输入以下代码行:

Blockchain.prototype.hashBlock = function(blockdata) {

}

在我们的hashBlock方法中,blockdata将是我们要生成哈希的区块数据的输入数据。

那么,我们如何将一个或多个数据块转换为哈希字符串呢?为了生成哈希数据,我们将使用一个名为SHA256的哈希函数。

理解 SHA256 哈希函数

SHA256哈希函数接收任何文本字符串,对该文本进行哈希处理,并返回一个固定长度的哈希字符串。

要更好地了解哈希数据的样子,请访问passwordsgenerator.net/sha256-hash-generator/。这是一个哈希生成器。如果你在文本框中输入任何文本,你将得到哈希数据作为输出。

例如,如果我们将CodingJavaScript放入文本框中,返回给我们的哈希看起来像以下截图中突出显示的那样:

在前面的截图中我们可以观察到的输出哈希看起来是随意的,因此有助于保持数据的安全。这就是为什么 SHA256 哈希如此安全的原因之一。

现在,如果我们在输入字符串中添加另一个字符,或者以任何方式改变我们的输入字符串,整个输出哈希将完全改变。例如,如果我们在输入字符串的末尾添加一个感叹号,输出哈希将完全改变。你可以在以下截图中观察到这一点:

你可以尝试通过在输入字符串的末尾添加新字符来进行实验。你会观察到随着我们添加或删除字符,整个输出哈希每次都会发生巨大的变化,从而生成新的随机模式。

你可能想观察与 SHA256 哈希相关的另一件事是,对于任何给定的输入,输出将始终相同。例如,对于我们的输入字符串codingJavaScript!,你将始终得到与之前截图中显示的相同的哈希输出。这是 SHA256 哈希的另一个非常重要的特性。对于任何给定的输入,从该输入返回的输出或哈希将始终相同。

因此,这就是 SHA256 哈希的工作原理。在下一节中,我们将在我们的hashBlock方法中实现 SHA256 哈希函数。

hashBlock 方法

让我们构建我们的hashBlock方法。在这个方法中,我们要使用 SHA256 哈希来哈希我们的区块数据。按照下面提到的步骤进行:

  1. 使用 SHA256 哈希函数,将其作为 npm 库导入。要做到这一点,去谷歌搜索栏中输入 SHA256,或访问www.npmjs.com/package/sha256。在这个网站上,你将看到我们需要在终端中输入的命令。我们需要在终端中输入以下命令:
npm i sha 256--save
  1. 完成后,按Enter。在以下命令中的--save将保存此库作为我们的依赖项。现在,在我们的区块链文件结构中,你可能会看到node_modules文件夹已经出现。在这个文件夹中,我们下载了 SHA256 库和所有其他依赖项。

  2. 要使用这个 SHA256 库,我们需要将库导入到我们的代码中,这样我们才能使用它。在我们的代码开头,输入以下行:

const sha256 = require('sha256');  

上述代码行指定了我们在blockchain.js文件中存储的 SHA256 哈希函数,存储为变量 SHA256。通过导入它,我们可以在我们的hashBlock方法中使用它。

  1. 现在,在我们的hashBlock方法中要做的第一件事是更改它所接受的参数。我们将用previousBlockHashcurrentBlockDatanonce替换blockData参数:
Blockchain.prototype.hashBlock = function(previousBlockHash, currentBlockData, nonce) {

}

这三个参数将是我们在hashBlock方法中要进行哈希的数据。所有这些数据将来自我们链中的一个单一区块,我们将对这些数据进行哈希,本质上是对一个区块进行哈希。然后我们将得到一个哈希字符串作为返回。

  1. 我们要做的第一件事是将所有这些数据转换为单个字符串,因此在我们的hashBlock方法中添加以下代码行:
const dataAsString = previousBlockHash + nonce.tostring()+ JSON.stringify( currentBlockData);

在上述代码中,previousBlockHash已经是一个字符串。我们的 nonce 是一个数字,所以我们将使用toString将其更改为字符串。此外,我们的currentBlockData将是一个对象,一个包含我们的交易或某种 JSON 数据的数组。它将是一个数组或一个对象,JSON.stringify将简单地将该数据(以及任何对象或数组)转换为字符串。一旦运行了整行代码,我们将简单地将所有传递的数据连接成一个单一的字符串。

  1. 现在,我们要做的下一件事是创建我们的哈希,如下所示:
const hash = sha256(dataAsString);

这是我们从区块或我们传递给函数的所有区块数据中创建哈希的方法。

  1. 我们要做的最后一件事就是简单地返回哈希,因此在完成这个方法之前,添加以下内容:
return hash;

这是我们的hashBlock方法将如何工作。在接下来的部分中,我们将测试这个方法,看看它是否完美地工作。

测试 hashBlock 方法

让我们在test.js文件中测试我们的hashBlock方法。与我们在之前的部分中所做的类似,在我们的test.js文件中,我们应该导入我们的区块链数据结构,创建一个新的区块链实例,并将其命名为bitcoin。现在,让我们测试我们的hashBlock方法:

  1. 为此,在我们的test.js文件中输入以下突出显示的代码行:
const Blockchain = require ('./blockchain'); 
const bitcoin = new Blockchain (); 

bitcoin.hashBlock();
  1. 我们的hashBlock方法需要三个参数:previousBlockHashcurrentBlockDatanonce。让我们在调用hashBlock方法的部分之前定义这些变量。我们将从定义previousBlockHash开始:
const previousBlockHash = '87765DA6CCF0668238C1D27C35692E11';

目前,这个随机字符串/哈希数据将作为我们的previousBlockHash的输入。

  1. 接下来,我们创建currentBlockData变量。这个currentBlockData将简单地是一个包含在这个区块中的所有交易的数组。我们将简单地使用这个区块中的交易作为我们的currentBlockData,所以在这个数组中,我们将不得不创建一些交易对象,如下所示:
const currentBlockData = [
    {
        amount: 10,
        sender: 'B4CEE9C0E5CD571',
        recipient: '3A3F6E462D48E9',  
    }  
]
  1. 接下来,至少复制这个交易对象三次,以在数组中创建更多的交易对象,然后根据需要对数据进行修改,目的是改变金额和寄件人和收件人的地址。这将使我们的currentBlockData成为一个包含三个交易的数组。

  2. 最后,我们必须在我们的hashBlock方法中分配nonce值:

const nonce = 100;
  1. 在定义了这些变量之后,我们调用hashBlock方法,并传递previousBlockHashcurrentBlockData参数,以及nonce
bitcoin.hashBlock(previousBlockHash, currentBlockData, nonce );
  1. 此外,让我们尝试将结果推送到终端窗口,以便我们可以观察它。为了做到这一点,我们将不得不对我们之前的代码进行一些微小的修改:
console.log(bitcoin.hashBlock(previousBlockHash, currentBlockData, nonce));

在这个测试案例中,我们使用所有正确的参数调用我们的hashBlock方法。当我们运行这个文件时,我们应该能够在终端窗口观察到哈希值。

  1. 现在保存这个test.js文件并运行它,检查我们是否得到了我们期望的输出。

  2. 打开你的终端窗口,输入node dev/test.js命令,让我们观察一下结果。你将能够观察到与我们的hashBlock方法输出相似的结果哈希值。

看起来我们的hashBlock方法工作得相当好。

  1. 尝试更仔细地探索一下hashBlock方法。正如前一节所解释的,如果我们改变传递给hashBlock方法的一些数据,将会完全改变我们返回的哈希值。

  2. 现在尝试通过更改寄件人或收件人地址中的一个字母来测试数据哈希的这个特性。然后保存文件,并再次使用node dev/test.js运行它。你将观察到一个完全不同的哈希数据作为输出,如下所示:

在上面的截图中,你可以观察到哈希数据和它们之间的差异。

现在,如果我们撤销了对寄件人或收件人地址所做的更改,并再次运行我们的哈希方法,我们将能够观察到与我们最初得到的相同的哈希值。这是因为我们传递的数据与第一次相同。你可以尝试尝试改变数据并观察输出,以进一步探索hashBlock方法。

经过这个测试,我们可以得出结论,我们的hashBlock方法完美地工作。

什么是工作证明?

接下来,我们要添加到我们的区块链数据结构中的方法是proofOfWork方法。这个方法对于区块链技术非常重要和必要。正是因为这个方法,比特币和许多其他区块链才如此安全。

现在,你一定对工作量证明(PoW)是什么感到好奇。如果我们看一下我们的区块链,每个区块链基本上都是一个区块列表。每个区块都必须被创建并添加到链中。然而,我们不希望随便创建并添加任何区块到链中。我们希望确保每个添加到链中的区块都是合法的,具有正确的交易和正确的数据。这是因为如果它没有正确的交易或正确的数据,那么人们可能会伪造自己拥有多少比特币,并从其他人那里窃取钱财。因此,每次创建新的区块时,我们首先必须通过 PoW 来确保它是一个合法的区块。

proofOfWork方法将接收currentBlockDatapreviousBlockHash。从我们提供的数据中,proofOfWork方法将尝试生成一个特定的哈希。在我们的示例中,这个特定的哈希将以四个零开头。因此,通过给定的currentBlockDatapreviousBlockHash,该方法将以某种方式生成一个以四个零开头的结果哈希。

现在让我们试着理解我们如何做到这一点。正如我们在前面的部分中学到的,从 SHA256 生成的哈希基本上是随机的。因此,如果得到的哈希基本上是随机的,那么我们如何从我们当前的区块生成一个以四个零开头的哈希呢?唯一的方法是通过反复试错或猜测和检查。因此,我们将不得不多次运行我们的hashBlock方法,直到最终有一次幸运地生成一个以四个零开头的哈希。

现在,你可能会想到我们的hashBlock方法的输入是previousBlockHashcurrentBlockDatanonce参数。当实际上,我们总是传递完全相同的数据时,这三个参数可能会生成多个不同的哈希,这个问题会让你感到困惑。此外,正如我们从上一节中所知,每当我们传入特定的数据时,我们总是会得到从该数据生成的相同的结果哈希。

那么,我们如何改变这些数据,而不改变我们的currentBlockDatapreviousBlockHash,但我们仍然可以得到一个以四个零开头的结果哈希呢?这个问题的答案是,我们将不断改变 nonce 值。

现在这一切可能看起来有点混乱,所以让我们试着通过对proofOfWork中实际发生的事情进行一些分解来澄清一下。

基本上,我们的proofOfWork中正在发生的事情是,我们将反复对我们的区块进行哈希,直到找到正确的哈希,这个哈希可以是以四个零开头的任何哈希。我们将通过不断增加 nonce 值来改变我们的hashBlock方法的输入。第一次运行我们的hashBlock方法时,我们将从 0 开始 nonce 值。然后,如果得到的结果哈希不以四个零开头,我们将再次运行我们的hashBlock方法,只是这一次我们将 nonce 值增加 1。如果我们再次没有得到正确的哈希值,我们将增加 nonce 值并再次尝试。如果这样不起作用,我们将再次增加 nonce 值并再次尝试。然后我们将不断运行这个hashBlock方法,直到找到一个以四个零开头的哈希。这就是我们的proofOfWork方法的功能。

你可能会想知道这个proofOfWork方法是如何确保区块链安全的。原因是为了生成正确的哈希,我们将不得不多次运行我们的hashBlock方法,这将消耗大量的能量和计算能力。

因此,如果有人想要回到区块链并尝试更改一个区块或该区块中的数据 - 也许是为了获得更多的比特币 - 他们将不得不进行大量的计算并使用大量的能量来创建正确的哈希。在大多数情况下,回头尝试重新创建已经存在的区块或尝试用自己的虚假数据重新挖掘已经存在的区块是不可行的。除此之外,我们的hashBlock方法不仅接受currentBlockData,还接受前一个BlockHash。这意味着区块链中的所有区块都通过它们的数据链接在一起。

如果有人试图回去重新挖掘或重新创建已经存在的区块,他们还必须重新挖掘和重新创建每一个在他们重新创建的第一个区块之后的每一个区块。这将需要大量的计算和能量,对于一个成熟的区块链来说是不可行的。一个人必须进去,通过工作证明重新创建一个区块,然后通过为每个区块进行新的工作证明来重新创建每个区块。这对于任何一个成熟的区块链来说都是不可行的,这就是为什么区块链技术如此安全的原因。

总结一下,我们的proofOfWork方法基本上会重复哈希我们的previousBlockHashcurrentBlockData和一个 nonce,直到我们得到一个以四个零开头的可接受的生成的哈希。

这一切可能看起来很压抑,现在可能有点混乱,但不用担心 - 我们将在接下来的部分构建proofOfWork方法,然后我们将用许多不同类型的数据进行测试。这将帮助您更加熟悉proofOfWork方法的功能以及它如何保护区块链。

创建proofOfWork方法

让我们构建我们在前一节中讨论过的proofOfWork方法:

  1. hashBlock方法之后,定义proofOfWork方法如下:
Blockchain.prototype.proofOfWork = function() {

}
  1. 这个方法接受两个参数:previousBlockHashcurrentBlockData
Blockchain.prototype.proofOfWork = function( previousBlockHash, currentBlockData) { 

}
  1. 在我们的方法内部,我们要做的第一件事是定义一个 nonce:
Blockchain.prototype.proofOfWork = function( previousBlockHash, currentBlockData) { 
    let nonce = 0;

}
  1. 接下来,我们要对我们的所有数据进行第一次哈希,所以输入以下突出显示的代码行:
Blockchain.prototype.proofOfWork = function( previousBlockHash, currentBlockData) { 
    let nonce = 0;
    let hash = this.hashBlock(previousBlockHash, currentBlockData,
     nonce); 
}

在前面的代码中,您可能会注意到我们使用了let这个术语,因为我们的 nonce 和 hash 都会随着我们在方法中的移动而改变。

  1. 我们接下来要做的是不断运行hashBlock方法,直到我们得到以四个零开头的哈希。我们将通过while循环来重复执行这个操作:
Blockchain.prototype.proofOfWork = function( previousBlockHash, currentBlockData) { 
    let nonce = 0;
    let hash = this.hashBlock(previousBlockHash, currentBlockData,
     nonce); 
    while (hash.substring(0, 4) !== '0000' {

 }  
}
  1. 如果我们创建的哈希值不以四个零开头,我们将希望再次运行我们的哈希,只不过这次使用不同的 nonce 值。因此,在while循环内,添加以下突出显示的代码行:
Blockchain.prototype.proofOfWork = function( previousBlockHash, currentBlockData) { 
    let nonce = 0;
    let hash = this.hashBlock(previousBlockHash, currentBlockData,
    nonce); 
    while (hash.substring(0, 4) !== '0000' {
        nonce++;
 hash = this.hashBlock(previousBlockHash, currentBlockData,
        nonce);
    }  
}

while循环内,我们再次运行我们的hashBlock方法,使用完全相同的数据,只是这次我们的 nonce 增加并且等于 1 而不是 0。这将是我们的 while 循环的第一次迭代。现在,在第一次迭代之后,生成的新哈希不具有前四个字符等于 0000 的特性。在这种情况下,我们将希望生成一个新的哈希。因此,我们的 while 循环将再次运行,nonce 值将再次增加到 2,并且将生成一个新的哈希。如果该哈希也不以四个零开头,那么while循环将再次运行,nonce 值将再次增加,并且将再次生成哈希。

我们的循环将继续这样做,直到得到以四个零开头的哈希。这可能需要很多次迭代。这可能发生 10 次,10,000 次或 100,000 次。

这个循环是所有计算将发生的地方,这就是为什么proofOfWork方法会消耗如此多的能量——有很多计算在进行。我们将继续通过while循环,直到生成一个以四个零开头的合适的哈希。当我们最终得到正确的哈希时,我们的while循环将停止运行,在proofOfWork结束时,它将简单地返回给我们提供有效哈希的 nonce 值:

Blockchain.prototype.proofOfWork = function( previousBlockHash, currentBlockData) { 
    let nonce = 0;
    let hash = this.hashBlock(previousBlockHash, currentBlockData, nonce); 
    while (hash.substring(0, 4) !== '0000' {
        nonce++;
        hash = this.hashBlock(previousBlockHash, currentBlockData, nonce);
    }  
    return nonce;
}

所以,这就是我们的proofOfWork方法是如何工作和验证哈希的。

在接下来的部分,我们将测试我们的proofOfWork方法,确保它能正常工作。我们还将研究为什么我们返回一个 nonce 值而不是返回哈希。

测试proofOfWork方法

让我们测试一下我们的proofOfWork方法,确保它能正常工作。我们将在test.js文件中测试这个方法。所以,让我们开始吧:

  1. 打开test.js文件。你可能会观察到数据与前一节文件中的以下截图类似,测试 hashBlock 方法

  1. 如果你的test.js文件中没有任何数据,就像在上面的截图中显示的那样添加到你的test.js文件中,然后你就可以开始测试数据了。

  2. 为了测试我们的proofOfWork方法,我们需要previousBlockHashcurrentBlockData。所以,在我们的测试用例中,去掉 nonce 值,并在我们的文件中添加以下代码行:

console.log(bitcoin.proofOfWork(previousBlockHash, currentBlockData));

现在,我们从proofOfWork方法中应该得到的结果是一个 nonce 值。我们的proofOfWork方法本质上是测试看看什么是与我们的块数据和previousBlockHash一起哈希的正确 nonce 值,以生成一个以四个零开头的结果块哈希。在这里,proofOfWork为我们找到了正确的 nonce。

  1. 保存这个文件,并在终端窗口中输入node dev/test.js命令来运行我们的测试。测试运行后,你会看到一个数字作为输出出现在屏幕上:

这个数字表示的是,我们的proofOfWork方法花了 27,470 次迭代来找到一个以四个零开头的哈希。

  1. 现在,为了深入了解整个过程,我们可以在while循环中记录我们尝试的每个哈希值。我们将不得不对我们的while循环进行一些微小的修改,就像下面的代码块中突出显示的那样:
while (hash.substring(0, 4) !== '0000' {
    nonce++;
    hash = this.hashBlock(previousBlockHash, currentBlockData,
    nonce);
    console.log(hash);
}

当我们现在运行我们的测试文件时,会发生的是我们应该能够在终端中看到 27,000 个不同的哈希值被记录出来。除了最后一个之外,这些哈希值都不会以四个零开头。只有最后一个被记录出来的哈希值应该以四个零开头,因为在我们的方法之后,这将终止并返回获得有效哈希值的 nonce 值。

现在再次保存我们的test.js文件。你现在可以在屏幕上观察到有大量不同的哈希值被记录到终端中:

你还可以观察到,每个被记录出来的哈希值的开头都不会连续出现四个零,直到我们得到最终值。

基本上,这里发生的是我们从currentBlockDatapreviousBlockHash和值为 0 的nonce生成哈希。然后,对于下一个哈希,我们将nonce递增 1。所以,输入数据都是一样的,但是nonce值会递增,直到获得有效的哈希。最终,在 27,470 次迭代中,通过 nonce 值获得了有效的哈希。

现在让我们尝试使用我们的hashBlock方法。在我们的dev/test.js文件中,删除proofOfWork方法,并添加以下代码行:

console.log(bitcoin.hashBlock(previousBlockHash, currentBlockData, nonce));

在上述代码中,对于 nonce,让我们输入值 27,470。这个值是我们从proofOfWork方法中获得的。

我们观察到的输出是使用正确的 nonce 值运行单个哈希,我们通过运行proofOfWork方法获得。通过这样做,我们应该在第一次尝试时生成一个以四个零开头的哈希。让我们保存并运行它。一旦测试运行,您将看到以四个零开头的单个哈希,如下面的截图所示:

proofOfWork是区块链技术的一个非常重要的部分。从测试结果中可以看出,计算它非常困难 - 我们花了超过 27,000 次迭代才生成正确的哈希。因此,proofOfWork需要大量的能量和许多计算,非常难以计算。

一旦我们有了正确的证明或生成所需哈希的 nonce 值,我们应该很容易验证我们是否有了正确的 nonce 值。我们可以通过简单地将其传递到我们的hashBlock方法中来验证这一点 - 我们将获得以四个零开头的哈希。

生成工作证明需要大量工作,但验证其正确性非常容易。因此,如果我们想要回到我们的区块链并检查一个块是否有效,我们只需对该块的数据与前一个块的哈希和从proofOfWork挖掘该块时生成的 nonce 进行哈希。如果这给我们返回一个以四个零开头的有效哈希,那么我们已经知道该块是有效的。

因此,从我们的测试中,我们可以得出结论,proofOfWork方法的工作符合预期。

创建创世区块

我们的区块链数据结构中还需要添加的一件事是创世块。但是什么是创世块?嗯,创世块只是任何区块链中的第一个块。

为了创建我们的创世区块,我们将在Blockchain()构造函数内部使用createNewBlock方法。转到dev/blockchain.js文件,并在区块链构造函数内部输入以下突出显示的代码行:

function Blockchain () {
    this.chain = [];
    this.pendingTransactions =[];
    this.createNewBlock();         
}

正如我们在前一节中观察到的,createNewBlock方法接受 nonce 的值,previousBlockHash和哈希作为参数。由于我们在这里使用createNewBlock方法创建创世区块,我们将不会有这些提到的参数。相反,我们只会传入一些任意的参数,如以下代码块中所示:

function Blockchain () {
    this.chain = [];
    this.pendingTransactions =[];
    this.createNewBlock(100, '0', '0');         
}

在上面的代码中,我们将 nonce 值传递为100previousBlockHash0,哈希值为0。这些都只是任意值;您可以添加任何您希望添加的值。

请注意,在创建我们的创世区块时传入这种任意参数是可以的,但是当我们使用createNewBlock方法创建新的区块时,我们将不得不传递参数的合法值。

现在保存文件,让我们在test.js文件中测试创世区块。

测试创世区块

dev/test.js文件中,我们将首先导入我们的区块链数据结构或区块链构造函数,然后将我们的区块链实例化为bitcoin。然后我们将以以下方式退出比特币区块链:

const Blockchain = require ('./blockchain');
const bitcoin = new Blockchain ();

console.log(bitcoin);

保存此文件,并在终端中键入node dev/test.js来运行测试。

运行测试后,我们可以观察到创世区块,如下面的截图所示:

在上面的截图中,对于链数组,您可以看到我们的链中有一个块。这个块是我们的创世块,它的 nonce 为 100,哈希为 0,previousBlockHash0。因此,我们所有的区块链都将有一个创世块。

摘要

在本章中,我们首先构建了构造函数,然后继续创建了一些令人惊奇的方法,比如createNewBlockcreatNewTransactiongetLastBlock等。然后我们学习了哈希方法,SHA256 哈希,并创建了一个为我们的区块数据生成哈希的方法。我们还学习了什么是工作量证明以及它是如何工作的。在本章中,您还学会了如何测试我们创建的各种方法,并检查它们是否按预期工作。在以后的章节中,我们将更多地与区块链进行交互,本章学到的方法将对我们非常有用。

如果您想更加熟悉区块链数据结构,建议您打开test.js文件,测试所有方法,尝试玩弄它们,观察它们如何一起工作,并且享受其中的乐趣。

在下一章中,我们将构建一个 API 来与我们的区块链进行交互和使用。那将是真正有趣的开始。

第三章:通过 API 访问区块链

构建区块链在上一章中,我们构建了我们的区块链数据结构的开端。在本章中,我们将构建一个 API,允许我们与我们的区块链进行交互。为了构建 API,我们将使用 Express.js 库创建一个服务器,然后我们将构建三个不同的端点,这些端点将允许我们与我们的区块链进行交互。

让我们开始从头构建我们的 API。在本章中,我们将涵盖以下主题:

  • 设置 Express.js

  • 构建 API 基础

  • 安装 Postman 和 body-parser

  • 构建/blockchain端点

  • 构建/transaction端点

  • 构建/mine端点

  • 测试端点

设置 Express.js

让我们开始构建我们的 API 或我们的服务器来与我们的区块链数据结构进行交互。我们将在一个新文件中构建我们的 API,并将其放入我们的dev文件夹中。让我们创建一个新文件并将其命名为api.js;这就是我们将构建整个 API 的地方:

安装 Express.js

现在,我们将使用一个名为Express.js的库来构建一个服务器或 API。让我们按照下面提到的步骤来安装它:

  1. 因此,前往 Google,搜索Express.js npm,并点击第一个链接(www.npmjs.com/package/express)。这将带您到以下页面:

  1. 我们必须将其安装为依赖项,因此我们必须在终端中运行以下命令:

现在我们在项目中有 Express 库作为依赖项。

使用 Express.js

使用 Express 非常简单:让我们看看如何使用它:

  1. 只需复制文档中的示例代码,并将其粘贴到我们的api.js文件中:
var express = require('express')
var app = express()

app.get('/', function (req, res) {
 res.send('Hello World')
})

app.listen(3000)

正如您所看到的,在我们的文件顶部,我们正在要求express,这是我们刚刚下载的库,然后我们正在创建一个app。这个app将帮助我们处理不同的端点或不同的路由。

例如,我们有一个get端点,它只是/。通过这个端点,我们发送Hello World的响应。整个服务器都在端口3000上监听。

  1. 要启动此服务器,我们转到终端并运行以下命令:
node dev/api.js
  1. 现在我们的服务器应该正在运行。我们可以通过在浏览器中点击get端点路由来测试这一点,这个路由将简单地是一个端口为3000的本地主机。

  2. 在浏览器中打开一个新标签,并输入localhost:3000。在这里你会看到文本 Hello World:

  1. 这是从端点发送给我们的响应。我们可以将文本更改为任何我们想要的,所以让我们将Hello World更改为Hello Coding JavaScript!
var express = require('express')
var app = express()

app.get('/', function (req, res) {
 res.send('Hello Coding JavaScript!')
})

app.listen(3000)
  1. 现在保存并重新启动服务器,通过在终端中再次运行以下命令:
node dev/api.js
  1. 刷新浏览器标签,您将看到以下输出:

就是这样!使用 Express 非常简单和容易。我们将使用 Express.js 库构建所有端点。

构建 API 基础

在本节中,我们将继续构建我们的区块链 API,然后我们将首先构建以下三个端点:

  • 第一个端点是/blockchain,它允许我们获取整个区块链,以便我们可以查看其中的数据。

  • 第二个端点是/transaction,它允许我们创建一个新的交易。

  • 第三个端点是/mine,它将允许我们使用我们在上一章中制作的proofOfWork方法来挖掘一个新的区块。这将是一个非常强大的端点,也将很有趣。

这基本上将成为我们的区块链 API 的基础。在dev/networkNode.js文件中,让我们定义这些端点如下:

const express = require('express');
const app = express();

app.get('/blockchain', function (req, res) {

});

app.post('/transaction', function(req, res) {

});

app.get('/mine', function(req, res) {

});

app,listen(3000);

现在,我们还要做的一件事是对listen方法进行一些修改:

app,listen(3000, function(){
    console.log('listening on port 3000...'); 

});

我们已经向这个方法添加了另一个参数,即一个函数。在这个函数内部,我们只是打印出Listening on port 3000字符串。我们这样做的原因只是为了当我们的端口实际运行时,我们会看到这个文本。让我们去我们的终端,再次运行我们的api.js文件:

如您所见,上面的截图显示我们正在监听端口3000。每当我们看到这个文本时,我们知道我们的服务器正在运行。

安装 Postman 和 body-parser

在这一部分,我们将在我们的环境中工作,使我们的开发过程变得更容易一些。我们要做的第一件事是安装一个叫做nodemon的新包。在我们的终端中的blockchain目录中,我们将写入npm i nodemon --save命令:

每当我们对文件进行更改并保存时,这个 nodemon 库会自动为我们重新启动服务器,这样我们就不必每次更改都要回到终端和代码之间来重新启动服务器。

要使用 nodemon,我们将打开我们的package.json文件。在"scripts"处,我们将添加一个新的脚本:

{
 "name": "javaScript-blockchain",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "start": "nodemon --watch dev -e js dev/api.js"
 }
 "author": "",
 "license": "ISC",
 "dependencies": {
     "express": "⁴.16.3",
     "nodemon": "¹.17.3",
     "sha256": "⁰.2.0"
 }
}

我们已经添加了"start": "nodemon --watch dev -e js dev/api.js"。这意味着当我们运行start命令时,我们希望nodemon监视我们的dev文件夹,并关注我们所有的 JavaScript 文件。每当这些 JS 文件中的一个被更改并保存时,我们希望 nodemon 为我们重新启动dev/api.js文件。保存package.json文件。现在,每当我们在dev文件夹中进行更改并保存时,我们的服务器将自动重启。让我们测试一下。

让我们去我们的终端。我们的服务器现在应该正在使用 nodemon:

我们使用npm start命令启动了服务器。您可以看到它正在监听端口3000。每当我们更改我们的 JS 文件并保存时,我们会看到我们的服务器会自动重启:

如您所见,服务器再次监听端口3000。这只是一个工具,我们用它来让开发对我们来说稍微容易一些。现在,我们想要使用的另一个工具叫做 Postman。

安装 Postman

Postman 工具允许我们调用任何我们的 post 端点,并通过我们的请求将数据发送到这些端点。让我们了解如何安装它:

  1. 转到www.getpostman.com并下载该应用程序。下载应用程序后,我们可以运行一些小测试,看看如何使用这个 Postman 应用程序来访问我们的/transaction端点。

  2. 下载 Postman 应用程序后打开它。您将看到类似以下截图的内容:

  1. 现在,在 Postman 应用程序中,我们将向http://localhost:3000/transaction发出 post 请求:

  1. 为了测试/transaction端点是否工作,让我们在输出中发送一些东西。在我们的/transaction端点中,我们添加了以下行:
app.post('/transaction', function(req, res) {
    res.send('It works!!!');
});
  1. 保存文件,现在当我们访问这个端点时,我们应该得到文本It works!!!的返回。点击发送按钮,您将得到输出,如下面的截图所示:

  1. 现在,大多数情况下,当我们在 API 中访问post端点时,我们都希望向其发送数据。例如,当我们访问/transaction端点时,我们希望创建一个新的交易。因此,我们必须向/transaction端点发送交易数据,比如交易金额、发送者和接收者。我们可以使用 Postman 来做到这一点,而且实际上非常简单。我们要做的是在我们的 post 请求的正文中发送一些信息。你可以通过点击 Body 选项卡来实现:

  1. 接下来,确保选中了原始选项,并从下拉列表中选择了 JSON(application/json)。你还可以看到我们已经创建了一个 JSON 对象,并放入了一些数据。我们已经将amount设置为20比特币,发送者的地址和接收者的地址。

请记住,所有内容都必须以 JSON 格式呈现,因此我们需要将所有引号都用双引号括起来,否则术语将无法工作。

  1. 为了测试我们是否在端点内收到了所有这些信息,我们将打印整个req.bodyreq.body就是我们在 JSON 对象中创建的信息:
app.post('/transaction', function(req, res) {
    console.log(req.body);
    res.send(`The amount of the transaction is ${req.body.amount}
     bitcoin.`);
});

正如你所看到的,我们还在响应中发送了一些不同的信息。我们在反引号中添加了一个句子,并且还使用了${req.body.amount}进行了一些字符串插值,这将返回amount

  1. 现在,为了使${req.body.amount}起作用,我们需要安装另一个库以访问这些信息。让我们回到终端;我们将退出当前监听端口3000的进程,并安装一个名为body-parser的包:

  1. 现在让我们再次用npm start启动我们的服务器。

  2. 当使用body-parser时,我们只需在文件顶部导入它:

const express = require('express');
const app = express();
const bodyParser = require('body-parser');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

为了使用body-parser库,我们添加了下面两行。这两行代码的作用是说明如果请求中带有 JSON 数据或表单数据,我们只需解析这些数据,以便在任何端点中访问。因此,无论我们访问哪个端点,我们的数据都会首先经过body-parser,以便我们可以访问数据,然后在接收数据的端点中使用。

  1. 现在我们使用了body-parser,我们应该能够访问这个金额。让我们保存api.js文件,并尝试发送请求,如下所示:

成功了!我们得到了返回的字符串,其中说明交易金额为 20 比特币。

在我们的终端中,由于我们记录了整个req.body,我们可以看到关于金额、发送者和接收者的所有信息都被显示出来:

太好了!现在,还有一件重要的事情要注意,那就是在本章的其余部分,你应该始终保持服务器运行,这意味着你应该始终运行npm start命令,这样我们才能使用我们的 API,访问不同的端点,并测试它是否有效。

构建/blockchain 端点

让我们继续构建我们的区块链 API。在这一部分,我们将与我们的/blockchain端点进行交互。这意味着我们将不得不从我们的blockchain.js文件中导入我们的区块链:

const Blockchain = require('./blockchain');

我们现在已经导入了我们的区块链数据结构或区块链构造函数。接下来,我们要创建一个区块链的实例。我们可以这样做:

const bitcoin = new Blockchain();

现在我们有了我们的区块链构造函数的一个实例,并且我们将其称为bitcoin。你可以自己决定叫什么,但我会简单地称其为bitcoin

让我们在/blockchain端点上继续构建。这个端点将会将整个区块链发送回给调用它的人。为了做到这一点,我们将添加一行代码来发送响应:

app.get('/blockchain', function(req, res) {
    res.send(bitcoin);
});

信不信由你,这就是我们为这个端点要做的全部。

测试/blockchain 端点

现在我们可以通过在浏览器中使用它来测试这个端点是否工作:

  1. 让我们转到我们的浏览器并访问localhost:3000/blockchain

  1. 正如你所看到的,我们得到了整个区块链。现在,你可能已经注意到这有点难以阅读,所以为了使其可读,让我们下载一个名为JSON 格式化程序的 Chrome 扩展。你可以在谷歌上搜索并将该扩展添加到你的 Chrome 浏览器中。安装完成后,再次刷新页面,你将得到以下输出:

正如你所看到的,我们以更易读的 JSON 格式得到了我们的数据。你可以看到我们有chain,其中有一项 - 我们的创世区块 - 以及pendingTransaction区块。这很酷,我们可以知道我们的/blockchain端点正在工作,因为我们得到了整个区块链。

构建/transaction 端点

在这一部分,我们将构建我们的交易端点。让我们按照下面提到的步骤进行:

  1. 在开始之前,请确保在处理我们的区块链时,你的服务器正在运行。我们可以通过在终端中运行npm start命令来做到这一点。

  2. 让我们转到我们的api.js文件并构建我们的交易端点。首先,去掉我们之前在/transaction端点中添加的示例代码,并在我们的区块链中创建一个新的交易。为此,我们将使用我们在第二章中构建的blockchain.js文件中的createNewTransaction方法,构建区块链

  3. 如你所知,我们的createNewTransaction方法接受三个参数:amountsenderrecipient

Blockchain.prototype.createNewTransaction = function(amount, sender, recipient) {
  const newTransaction = {
    amount: amount,
    sender: sender,
    recipient: recipient
  };

  this.pendingTransactions.push(newTransaction);

  return this.getLastBlock()['index'] + 1;
};
  1. 这个方法返回我们新交易将被添加到的区块编号或索引。这就是我们创建交易所需的一切,所以在我们的/transaction端点中,我们将添加以下行:
app.post('/transaction', function(req, res) {
  const blockIndex = bitcoin.createNewTransaction(req.body.amount,
   req.body.sender, req.body.recipient) 
});
  1. 在我们的端点中,我们假设所有这些数据都是通过req.body从调用这个端点的人那里发送过来的。结果将保存在blockIndex中,这就是我们将发送回给调用这个端点的人的内容。我们将把它作为一个note发送回去:
app.post('/transaction', function(req, res) {
  const blockIndex = bitcoin.createNewTransaction(req.body.amount,
  req.body.sender, req.body.recipient) 
 res.json({ note:`Transaction will be added in block
    ${blockIndex}.`});
});

正如你所看到的,这个注释将告诉我们交易将被添加到哪个区块。我们使用了字符串插值来传递blockIndex的值。让我们保存这个文件并使用 Postman 测试这个端点。

测试/transaction 端点

现在让我们转到 Postman 并应用与我们之前设置的类似的设置:

我们已经选择了 POST 请求,并且我们的目标是/transaction端点。在 Body 选项卡中,我们已经勾选了 raw,并且文本已经选择为 JSON 格式。我们在 JSON 对象中传入了amountsenderrecipient的值,这将成为我们的req.body,并且我们将发送所有的交易数据到这个对象上。借助于我们在/transaction端点中提到的req.body,我们可以访问金额、发送者的地址和接收者。

现在让我们测试这个端点:

正如你所看到的,当我们在 Postman 上点击发送按钮时,我们得到了交易将被添加到第 2 个区块的输出。我们之所以在这里得到第 2 个区块,是因为在我们初始化区块链时已经创建了一个区块,这就创建了创世区块。因此,这个交易被添加到了第 2 个区块。

我们可以测试确保这个端点工作正确的另一种方法是访问我们的/blockchain端点。当我们访问这个端点时,我们应该期望得到我们整个的区块链。在那个区块链中,应该有一个单独的区块 - 我们的创世区块 - 还应该有一个待处理的交易,这就是我们刚刚创建的交易。让我们转到浏览器,访问localhost:3000/blockchain

正如你所看到的,整个对象就是我们整个的区块链 - 第一部分是我们的链,其中包含创世区块,第二部分是我们的待处理交易,我们刚刚创建。我们的/transaction端点完美地工作。

构建/mine 端点

让我们构建我们的区块链 API 的最终端点:挖矿端点,这将挖矿并创建一个新的区块:

  1. 为了创建一个新的区块,我们将使用我们在blockchain.js文件中已经定义的createNewBlock方法。让我们转到我们的api.js文件,并在/mine端点中创建一个新的区块:
app.get('/mine', function(req, res) {
    const newBlock = bitcoin.createNewBlock();
});
  1. 这个createNewBlock方法接受三个参数:noncepreviousBlockHashhash
Blockchain.prototype.createNewBlock = function(nonce, previousBlockHash, hash) {
  const newBlock = {
    index: this.chain.length + 1,
    timestamp: Date.now(),
    transactions: this.pendingTransactions,
    nonce: nonce,
    hash: hash,
    previousBlockHash: previousBlockHash
  };

  this.pendingTransactions = [];
  this.chain.push(newBlock);

  return newBlock;
};
  1. 现在我们必须进行计算,以获得所有这三个数据,所以让我们开始。让我们从获取上一个区块开始,以便我们可以获取它的 hash:
app.get('/mine', function(req, res) {
  const lastBlock = bitcoin.getLastBlock();
  const previousBlockHash = lastBlock['hash'];

正如你所看到的,我们已经创建了lastBlock,它是我们链中的最后一个区块 - 或者是我们新区块的上一个区块。为了获取上一个区块的hash,我们创建了previousBlockHash。有了这个,我们现在可以有我们的previousBlockHash,这是我们createNewBlock方法下一个需要的参数之一。

  1. 接下来,让我们获取我们的nonce。为了为我们的区块生成一个nonce,我们需要生成一个proofOfWork,这是我们在blockchain.js文件中创建的:
Blockchain.prototype.proofOfWork = function(previousBlockHash, currentBlockData) {
  let nonce = 0;
  let hash = this.hashBlock(previousBlockHash, currentBlockData,
  nonce);
  while (hash.substring(0, 4) !== '0000') {
    nonce++;
    hash = this.hashBlock(previousBlockHash, currentBlockData,
    nonce);
  }

  return nonce;
};
  1. 在我们的/mine端点,我们将添加以下行:
const nonce = bitcoin.proofOfWork(previousBlockHash, currentBlockData);
  1. 因此,从我们的proofOfWork方法中,我们将得到一个nonce返回给我们。让我们将其保存为我们的nonce变量。我们的proofOfWork方法接受两个参数:previousBlockHash,我们已经有了,和currentBlockData。让我们定义我们的currentBlockData
const currentBlockData = {
    transactions: bitcoin.pendingTransactions,
    index: lastBlock['index'] + 1
  };

我们有我们的currentBlockData作为一个对象,其中包含数据。这些数据将简单地包括这个区块中的transactions,还有一个index,这是我们将要创建的新区块的索引;我们的lastBlock的索引加 1。currentBlockData对象将简单地是这个新区块中存在的transactions和它的index。有了这个,我们现在可以计算我们的nonce,就像我们用我们的previousBlockHashcurrentBlockData一样。

  1. 现在,我们的createNewBlock方法必须接受的最后一个参数是这个新区块的hash,所以让我们现在计算一下。为了创建这个新区块的hash,我们将使用我们的hashBlock方法。我们将在我们的/mine端点中添加以下行:
const blockHash = bitcoin.hashBlock(previousBlockHash, currentBlockData, nonce);

如你所知,我们已经在blockchain.js文件中创建了hashBlock方法。这个方法接受三个参数:previousBlockHashcurrentBlockDatanonce。我们已经有了所有这些参数,所以我们正在调用它,并将结果保存在一个名为blockHash的变量中。

  1. 我们现在有了我们运行createNewBlock方法所需的所有参数,所以让我们分配这些参数:
const newBlock = bitcoin.createNewBlock(nonce, previousBlockHash, blockHash);

这里发生的事情非常棒。正如你所看到的,有很多不同的计算涉及到创建这个新区块,我们能够通过使用我们的区块链数据结构来进行所有这些计算。这是一个非常强大的数据结构,我们的区块链现在可以通过使用proofOfWork来挖掘新的区块,这与许多其他区块链的功能类似。

  1. 在这一点上,我们已经创建了我们的新区块,我们真正需要做的就是将响应发送给挖掘这个区块的人。接下来,我们将在我们的/mine端点中添加以下行:
res.json({
  note: "New block mined successfully",
  block: newBlock
});

我们只是简单地发送一个说明新块挖掘成功的消息,以及我们刚刚创建的newBlock。现在,发送这个newBlock不会以任何方式影响我们的区块链。我们发送newBlock是为了让创建或挖掘这个新块的人知道它的样子。

  1. 现在只剩下一件事情要做:每当有人挖掘一个新块,他们都会得到一个奖励。我们所要做的就是创建一个交易,给挖掘这个新块的人发送一点比特币作为奖励。为此,在/mine端点内部,我们将创建一个新的交易:
bitcoin.createNewTransaction(12.5, "00", nodeAddress);

目前,在 2018 年,真正的比特币区块链中挖掘新块的奖励是 12.5 比特币。为了保持与真正的比特币一致,我们的奖励也将是12.5比特币。作为发送者地址,我们已经放入了值00。这样,每当我们在我们的网络上查看交易时,我们知道如果有一个交易是从地址00发出的,那就是一个挖矿奖励。

现在我们只需要一个接收者的地址,nodeAddress。我们需要把12.5比特币发送给挖掘新块的人,但是怎么找到呢?嗯,我们将把这个奖励发送给我们当前所在的节点,也就是我们正在使用的整个 API 文件。我们可以把整个 API 都当作比特币区块链中的一个网络节点。

在未来的章节中,我们将拥有我们 API 的多个实例,并且它们将作为大型干净区块链中的不同网络节点。现在,每当我们访问我们创建的任何端点时,我们总是只与这一个网络节点进行通信。然而,由于我们知道所有的区块链技术都是分散的,并且托管在许多不同的网络节点上,随着我们进一步进行,我们将创建更多的网络节点。但是现在,我们整个的区块链只托管在这一个网络节点上。

现在,每当我们访问/mine端点时,我们都希望奖励这个节点挖掘新块。为了给这个节点应得的12.5比特币奖励,我们需要一个地址来发送比特币,所以让我们现在为这个节点创建一个地址。

为了为这个节点创建一个地址,我们将使用我们的终端导入一个叫做uuid的新库:

一旦你输入了npm i uuid --save命令并按下Enter,包就会被添加。你可以使用npm start命令重新启动服务器。

现在让我们在api.js文件的顶部部分导入我们的新的uuid库:

const uuid = require('uuid/v1');

正如你所看到的,我们已经导入了uuid库的第 1 个版本。这个库为我们创建了一个唯一的随机字符串,我们将使用这个字符串作为这个网络节点的地址。为此,我们将添加以下行:

const nodeAddress = uuid().split('-').join('');

关于我们从这个库得到的字符串,我们想要改变的一件事是,它里面有一些破折号——我们不希望地址里有任何破折号。在这里,我们只是简单地将该字符串在所有的破折号上分割,然后用一个空字符串重新连接。我们将得到的nodeAddress是一个随机字符串,保证是独一无二的。我们真的希望这个字符串是独一无二的,因为我们不希望有两个节点有相同的地址,否则我们会把比特币发送给错误的人,那就不好了。现在我们只需将这个nodeAddress变量传递给我们的createNewTransaction方法。

在下一部分,我们将测试我们的/mine端点,以及我们的/transaction/blockchain端点,以确保它们都能正确地工作和互动。

测试端点

在这一部分,我们将测试我们的/mine端点,以及我们的/transaction/blockchain端点,以确保一切都能很好地协同工作。

在测试之前,最好将proofOfWork方法中的console.log语句删除。这是因为有它只会让你的程序工作更加艰难,因此计算所需的时间会更长。

/mine 端点测试

首先,让我们测试我们在上一节中构建的/mine端点。让我们转到浏览器,访问localhost:3000/blockchain

现在,我们有整个区块链,链中有一个区块 - 我们的创世区块 - 也没有待处理交易。

现在让我们打开另一个标签页,点击我们的/mine端点。这应该为我们挖矿并创建一个新的区块:

我们收到了一条新的区块成功挖掘的消息。我们还收到了我们的新区块,并且我们可以看到区块上的所有数据。它里面有一个哈希,还有前一个区块的哈希,即创世区块,以及一个交易。也许你会想,我们并没有创建交易,那么这笔交易是从哪里来的呢?实际上,这笔交易是我们放入端点的挖矿奖励,即12.5比特币的挖矿奖励交易。看起来我们的挖矿端点运行良好。

测试/blockchain 端点

为了测试并确保我们确实创建了这个新区块,我们可以转回到我们的/blockchain端点并刷新页面:

成功了。我们现在的链中有两个区块:一个是创世区块,另一个是我们刚刚创建的区块。第二个区块中也有交易,其中包括奖励。

让我们再挖掘一个区块来再次测试。转到我们的/mine端点并刷新页面:

我们刚刚挖掘了另一个区块,这是我们的第三个区块。我们可以看到我们得到了timestamp和另一笔交易,即挖矿奖励,还有我们的其他数据。现在让我们转回到我们的/blockchain端点并刷新页面:

正如你所看到的,我们有三个区块。区块 3 是我们刚刚创建的区块,里面有我们的挖矿奖励交易。还有一件事要注意的是,我们的previousBlockHash实际上与我们的区块 2 的hash对齐。这有助于保护我们的区块链,这很好。

测试/transaction 端点

现在让我们使用我们的/transaction端点创建一些交易。为此,请转到 Postman,确保设置与之前相同,并进行以下更改:

我们将amount设置为1000比特币。我们将保留发送方和接收方地址不变,但你可以根据需要进行更改。一旦我们提交到/transaction端点,我们应该得到文本交易的响应,该交易将被添加到第 4 个区块中,我们确实得到了这个响应。这笔交易被添加到第 4 个区块,因为我们的链中已经有了三个区块。

让我们进行另一个示例交易。在这里,我们将amount更改为50比特币,并对发送方和接收方的地址进行一些更改。因此,当我们发送此请求时,我们应该得到相同的响应:交易将被添加到第 4 个区块。这是因为我们还没有挖掘新的区块。让我们试一试:

这很有效。现在让我们再次获取整个区块链。这次,我们应该期望得到与我们刚刚创建的相同的区块链和两笔待处理交易。让我们刷新页面并查看输出:

你会注意到这里有三个区块和两笔待处理交易。现在,如果我们转到我们的/mine端点并刷新页面,这两笔待处理交易将被添加到第 4 个区块中:

我们已成功挖掘了一个新的区块。它包含我们的数据,也有三笔交易。前两笔交易是我们在 Postman 中创建的,第三笔是我们的挖矿奖励交易。现在,如果我们回到我们的/blockchain端点并刷新它,我们会看到两笔待处理的交易已经消失,并且它们已被添加到第 4 个区块中。

正如您所看到的,第 4 个区块包含了所有三笔交易,我们的pendingTransactions现在为空。效果很好。现在,我鼓励您创建更多的交易并挖掘另一个区块,以确保一切都正常工作。

通过构建整个 API 和区块链,并真正理解代码的工作原理,更容易理解区块链技术的实际运作方式,您也会意识到其中很多实际上并不那么复杂。

在测试这些端点的任何时候,如果您对文件进行更改并保存,服务器将重新启动。这将导致区块链的新实例,这意味着到目前为止创建的所有内容都将被清除。

摘要

在本章中,我们学习了如何在项目中设置 Express.js,以及如何使用它来构建我们的 API/服务器。然后我们安装了 Postman,并了解了如何使用它来测试我们的端点。之后,我们继续构建了服务器的各种端点,并测试它们以验证它们是否正常工作。

在下一章中,我们将创建一个节点网络或去中心化网络来托管我们的区块链,就像在现实世界中托管的那些一样。

第四章:创建分散的区块链网络

在本章中,让我们专注于构建分散的区块链网络。我们的区块链目前的工作方式是我们有一个单一的区块链,而访问它的唯一方式是通过 API:我们的单一服务器。这个服务器非常集中,这并不好,因为 API 完全控制着区块链和添加到其中的数据。

在现实世界中,所有区块链技术都托管在分散网络中。在本章中,这就是我们要专注于构建的内容。我们将通过创建各种 API 实例来构建一个分散的区块链网络。这些 API 实例中的每一个都将成为我们区块链网络中的一个网络节点。所有这些节点将共同工作来托管我们的区块链。

这样一来,不仅仅是一个单一的网络节点完全控制着我们的区块链。相反,我们的区块链托管在整个分散网络中。这样,如果我们的网络中有一个坏的参与者,或者有人试图欺骗系统,我们可以参考其他网络节点来查看我们的区块链内部应该是什么样的真实数据,以及我们的区块链实际上应该是什么样的。

我们的区块链托管在分散网络中非常强大,因为它极大地增加了我们的区块链的安全性,因此我们不必只信任一个单一实体来处理我们所有的数据。

在本章中,我们将涵盖以下主题:

  • 学习如何创建和测试多个节点

  • currentNodeUrl添加到我们的网络

  • 为分散网络添加新的端点

  • 构建/register-and-broadcast-node端点

  • 构建和测试/register-node端点

  • 添加和测试/register-nodes-bulk端点

  • 测试所有网络端点

让我们开始创建我们的分散网络。

创建多个节点

让我们从构建分散网络开始:

  1. 要创建我们的分散区块链网络,我们首先需要对我们的api.js文件进行一些修改。

  2. 在我们的分散网络中,我们将有多个 API 实例,每个实例都将充当网络节点。由于我们将处理多个网络节点,最好将我们的api.js文件重命名为networkNode.js以便易于引用。

  3. 要设置分散网络,我们将不得不多次运行networkNode.js文件。每次运行文件时,我们希望它作为不同的网络节点。我们可以通过在每次运行时在不同的端口上运行文件来实现这一点。为了每次都有不同的端口值,我们将把端口作为一个变量。为此,在我们的dev/networkNode.js的代码开头添加以下行:

const port = process.argv[2]; 
  1. 接下来,打开package.json文件并对start命令进行修改。我们要做的是转到命令的末尾,并传递我们想要网络节点运行的端口号的变量。在我们的示例中,我们希望我们的网络节点在端口号3001上运行。因此,在启动命令的末尾传递3001作为变量:
"start": "nodemon --watch dev -e js dev/api.js 3001"

为了访问这个变量,我们在我们的networkNode.js文件中传递了process.argv变量。那么,process.argv变量是什么?这个变量简单地指的是我们运行启动服务器的start命令。

您可以将前面的start命令视为元素数组。命令的第一个和第二个元素由“nodemon --watch dev -e js dev/api.js”组成,命令的第三个元素是3001变量。

如果您想向命令添加更多变量,只需在其后添加更多变量。

因此,为了在start命令中访问端口变量,我们将变量作为process.argv [2]传递,因为这个数组从0索引开始,我们的端口变量是开始命令中的第三个元素。为了简化这个过程,我们可以通过在位置 2 处声明process.argv来访问3001变量。因此,我们可以在dev/networkNode.js文件中访问我们的port变量。

  1. 接下来,我们想要使用port变量。因此,在dev/networkNode.js文件中,转到底部,我们已经提到了以下代码:
app.listen(3000, function() {
    console.log('Listening on port 3000...');
});
  1. 一旦找到这个,对其进行如下突出显示的修改:
app.listen(port, function() {
    console.log(`Listening on port ${port}...`);
});

在前面的代码块中,我们用我们的port变量替换了硬编码的3000端口号。我们还通过使用字符串插值和传递端口变量,将Listening on port 3000...改为Listening on port ${port}...。现在,当我们运行networkNode.js文件时,它应该在端口3001上监听,而不是在端口3000上。

  1. 在运行networkNode.js文件之前,我们需要更改的一个小细节是在package.json文件的start命令中,我们需要将api.js文件的名称更改为networkNode.js

  2. 现在我们已经准备好通过传入我们想要的任何端口变量来运行networkNode.js文件。

  3. 让我们运行networkNode.js文件。在终端窗口中,输入npm start。通过输入这个命令,服务器应该开始监听端口3001,正如我们在下面的截图中所观察到的:

  1. 从前面的截图中,我们可以观察到服务器正在监听端口3001。我们可以通过在浏览器中输入localhost:3001/blockchain来进一步验证这一点。您应该看到类似于下面截图所示的输出:

  1. 从前面的截图中,我们可以看到我们的区块链现在托管在端口3001上,而不是在端口3000上。如果我们去端口3000,就会像下面的截图所示的那样,什么也没有。

运行多个networkNode.js实例

接下来我们要做的事情是运行多个networkNode.js实例。为此,我们将在package.json文件中添加一些命令:

  1. 首先,在package.json文件中,我们必须将"start"命令更改为"node_1"。现在,当我们运行此命令时,它将启动我们的第一个节点,即端口3001上的节点。让我们试一试。

  2. 保存文件,转到终端,并通过输入^C%取消之前的进程。在这样做之后,而不是输入npm start,输入npm run node_1。通过这个命令,运行我们的node_1在端口3001上:

在这个过程中,我们真正做的是将npm start命令更改为npm run node_1

  1. 对于我们的分散网络,我们希望同时运行几个这样的节点。让我们回到我们的package.json文件,并添加类似于"node_1"的更多命令。为此,将"node_1": "nodemon --watch dev -e js dev/networkNode.js 3001",命令复制四次,然后对这些命令进行修改,如下面的截图所示:

  1. 现在,保存这个修改,让我们回到终端并启动其他网络节点。从上一次运行中,我们有第一个节点node_1在端口3001上运行。对于这次运行,我们将希望在端口3002上运行第二个节点node_2。因此,只需输入npm run node_2然后按Enter。我们将在屏幕上观察到以下输出:

我们现在有一个运行在端口3001上的网络节点,另一个运行在端口3002上的网络节点。按照类似的过程在剩下的端口上运行剩下的网络节点。

为了更好地可视化和易于理解,建议您尝试在终端窗口的不同标签上运行每个节点。

通过遵循这个过程,我们实际上正在创建我们的networkNode.js文件的五个不同实例。因此,基本上,我们有五个不同的网络节点在运行。

在浏览器中,我们可以通过更改localhost:3001/blockchain中的端口号来检查这些网络节点中的每一个。通过这样做,我们将在不同的端口上得到不同的区块链。

测试多个节点

我们将继续探索上一节中创建的五个独立网络节点。到目前为止,您可能已经运行了所有五个网络节点。如果没有,请回到上一节,了解如何使这些节点中的每一个运行是值得推荐的。我们目前拥有的,即五个独立运行的网络节点,实际上并不是一个网络。我们只有五个独立的节点或我们的 API 的五个独立实例,但它们没有以任何方式连接。为了验证这些网络节点没有连接,我们可以进行一些测试:

  1. 所以,让我们转到 Postman,并尝试通过在我们正在运行的不同网络节点上命中/transaction端点来进行一些不同的交易。

  2. 我们要进行的第一笔交易将是到我们托管在端口3001上的网络节点。因此,让我们进入正文,并输入一些随机交易数据,如下面的屏幕截图所示:

  1. 我们的交易数据有 30,000 比特币,我们将其发送到端口3001上托管的网络节点。单击发送按钮,如果交易成功,您将获得以下响应,如下面的屏幕截图所示:

  1. 现在让我们向托管在端口3003上的网络节点进行 10 比特币的交易。然后单击发送按钮将交易发送到端口3003上的网络节点。在这里,您也将看到类似的响应。

  2. 现在我们已经将交易数据发送到网络节点,让我们验证一下。转到浏览器,然后转到localhost:3001/blockchain,然后按Enter。您将看到一个类似的响应,如下面的屏幕截图所示:

从前面的屏幕截图中,您可以看到我们有一个未决的 30,000 比特币交易。这是我们刚刚添加的交易之一。

  1. 现在,在另一个标签中,如果我们转到localhost:3002/blockchain,您将看到我们没有未决交易,因为我们没有向这个网络节点发送任何交易:

  1. 接下来,如果我们转到localhost:3003/blockchain,您将看到我们有一个未决的 10 比特币交易:

这是我们进行的另一笔交易。

如果我们去localhost:3004/blockchainlocalhost:3005/blockchain,那里应该没有交易,因为我们没有向这些网络节点发送任何交易。

从这次测试中我们可以得出的结论是,尽管我们有五个不同的网络节点并行运行,但它们没有以任何方式连接。因此,本章的主要目的将是将所有网络节点连接到彼此,以建立一个去中心化的网络。

添加当前节点 URL

在测试我们的节点之后,我们要做的下一件事是稍微修改package.json中的命令。我们要这样做的原因是因为我们希望我们的每个网络节点都知道它们当前所在的 URL。例如,它们可能在http://localhost:3001localhost:3002localhost:3003等上。因此,我们希望每个节点都知道它所托管的 URL。

在我们的package.json中,作为我们每个命令的第三个参数,我们将添加节点的 URL。因此,我们第一个节点的 URL 将简单地是http://localhost:3001。很可能对于我们的第二个节点,它将是http://localhost:3002。同样,您可以像下面的截图所示为其余节点添加 URL:

添加 URL 后,保存文件。现在我们已经将每个节点的 URL 作为参数传递给我们用来运行每个节点的命令。因此,我们应该可以在我们的文件内访问这些 URL,就像我们在我们的文件内访问我们的端口变量一样。

现在让我们转到blockchain.js文件,并在定义常量的部分,我们将输入以下内容:

const currentNodeUrl = process.argv[3];

使用此命令,我们应该可以通过使用currentNodeUrl变量访问当前节点的 URL。

现在我们应该将currentNodeUrl分配给我们的Blockchain数据结构。我们通过在我们的function Blockchain {}内输入以下突出显示的代码行来执行此操作:

function Blockchain() {
       this.chain = [];
       this.pendingTransactions = [];

       this.currentNodeUrl = currentNodeUrl;

       this.createNewBlock();
};

接下来,我们还希望我们的区块链能意识到我们网络中的所有其他节点。因此,我们将在上述突出显示的代码行下面添加以下代码:

this.networkNodes = [];

在接下来的部分,我们将用我们网络中所有其他节点的节点 URL 填充这个数组,以便每个节点都能意识到我们区块链网络中的所有其他节点。

新端点概述

在我们的区块链中,我们现在想要创建一个网络,并且有一种方法来注册我们的所有不同节点。因此,让我们创建一些端点,这将使得我们可以向我们的网络注册节点成为可能。

定义/register-and-broadcast-node端点

我们创建的第一个端点将是/register-and-broadcast-node,定义如下:

app.post('/register-and-broadcast-node', function (req, res) {

});

上述端点将注册一个节点并将该节点广播到整个网络。它将通过在req body 中传递我们要注册的节点的 URL 来执行此操作。因此,在上述端点内输入以下内容:

const newNodeUrl = req.body.newNodeUrl;

我们现在不会构建这个端点,但是当我们在后面的部分中使用它时,我们将发送要添加到我们网络中的新节点的 URL。然后我们将进行一些计算并将节点广播到整个网络,以便所有其他节点也可以添加它。

创建/register-node 端点

/register-node将是我们将添加到我们网络中的下一个端点。定义如下:

app.post('/register-node', function (req, res) {

});

这个端点将在网络中注册一个节点。

register-and-broadcast-noderegister-node端点之间的区别

现在让我们试着理解/register-and-broadcast-node/register-node端点的不同之处。基本上,这里将发生的是,每当我们想要向我们的网络注册一个新节点时,我们将会命中/register-and-broadcast-node端点。这个端点将在自己的服务器上注册新节点,然后将这个新节点广播到所有其他网络节点。

这些网络节点将在/register-node端点内简单地接受新的网络节点,因为所有这些节点所要做的就是简单地注册广播节点。我们只希望它们注册新节点;我们不希望它们广播新节点,因为这已经发生了。

如果网络中的所有其他节点也广播新节点,那将严重影响我们的区块链网络性能,并导致一个无限循环,导致我们的区块链崩溃。因此,当所有其他网络节点接收到新节点的 URL 时,我们只希望它们注册而不广播。

定义/register-nodes-bulk 终端

在本节中,我们将构建的最终终端将是/register-nodes-bulk终端:

app.post('/register-nodes-bulk', function (req, res) {

});

此终端将一次注册多个节点。

了解所有终端如何一起工作

在这个阶段了解所有这些终端可能会有点混乱,所以让我们尝试通过图表来理解。在下图中,我们有我们的区块链网络:

现在假设这五个网络节点已经相互连接,从而形成我们的去中心化网络。另外,假设我们想要将托管在localhost:3009上的节点添加到我们的网络中。

我们要做的第一件事是将该节点添加到我们的网络中,即在我们的网络节点中的一个上命中register-and-broadcast-node终端:

当我们命中register-and-broadcast-node终端时,我们需要发送我们想要添加到我们的网络中的新节点的 URL。对于我们的示例,URL 是localhost:3009。这是向我们的网络添加新节点的第一步。我们必须使用新节点的 URL 作为数据命中我们的register-and-broadcast-node终端。

在上图中,我们命中的网络节点将在其自己的节点上注册这个新的 URL,然后将这个新节点的 URL 广播到网络的其余部分。我们的网络中的所有其他节点将在register-node终端接收到这些数据:

我们将在所有其他网络节点上命中register-node终端,因为我们不需要再广播数据,我们只需要注册它。

现在,在所有其他网络节点上注册了新的 URL 后,我们的原始节点将向新节点发出请求,并命中register-node-bulk终端:

此外,原始节点将传递所有其他节点的 URL。因此,此调用将注册网络中已经存在的所有其他节点与新节点。

此时,该节点现在是网络的一部分,网络中的所有节点也将意识到网络中存在的所有其他节点。

现在让我们再次回顾整个过程。我们要做的第一件事是在我们的网络中的一个节点上命中/register-and-broadcast-node终端,以添加一个新节点到我们的网络中。此终端将注册新节点的 URL,然后将该新 URL 广播到网络中的所有其他节点。广播完成后,我们命中的原始网络节点将向新网络节点发送请求,并命中register-nodes-bulk终端。通过这样做,它将注册网络中的所有其他节点与我们的新节点。

因此,当整个过程完成时,所有这些节点将成为我们去中心化的区块链网络的一部分,并且它们将相互注册。

这就是这三个终端如何一起工作的。在接下来的部分,我们将构建register-and-broadcast-node终端。

构建/register-and-broadcast-node 终端

让我们开始构建我们的注册和广播节点终端。这个终端的功能将是向自身注册新节点,然后将新节点广播到网络中已经存在的所有其他节点。所以,让我们开始构建这个终端:

  1. 从前面的部分,在dev/networkNode.js文件中,我们已经有以下代码:
app.post('/register-and-broadcast-node', function(req, res) {
       const newNodeUrl = req.body.newNodeUrl;

在这里,我们定义了一个名为newNodeUrl的变量,这个newNodeUrl数据将被传递到请求体中,类似于我们将交易数据传递到交易端点的方式。有了newNodeUrl的访问权限,我们想要做的第一件事是注册节点到节点的register-and-broadcast-node端点。

  1. 要注册它,我们所要做的就是将newNodeUrl放入我们的blockchain数据结构的networkNodes数组中。为此,在前面的代码块中添加以下突出显示的代码:
app.post('/register-and-broadcast-node', function(req, res) {
       const newNodeUrl = req.body.newNodeUrl;
      bitcoin.networkNodes.push(newNodeUrl); 
  1. 通过添加上述代码行,我们将newNodeUrl推送到networkNodes数组中。只有在数组中newNodeUrl尚未存在时才这样做。通过以下if语句来检查:
app.post('/register-and-broadcast-node', function(req, res) {
       const newNodeUrl = req.body.newNodeUrl;
      if (bitcoin.networkNodes.indexOf(newNodeUrl) == -1) bitcoin.networkNodes.push(newNodeUrl);

if语句正在检查newNodeUrl是否已经存在于networkNodes数组中。如果不存在,则将其添加到数组中。因此,借助上述代码块,newNodeUrl将被注册到register-and-broadcast-node端点。

  1. 现在我们已经注册了newNodeUrl,现在我们要做的是将其广播到网络中的所有其他节点。为此,在 if 块之后添加以下代码行:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    //... '/register-node' 

}

在上述代码块中,对于已经存在于网络中的每个网络节点,或者对于已经存在于networkNodes数组中的每个网络节点,我们都希望通过命中注册节点端点来注册我们的newNodeUrl。为此,我们将不得不在这个端点向每个单独的节点发出请求。

  1. 我们将通过导入一个新的库来进行此请求。让我们去终端导入这个库。在终端中,我们将取消我们的第一个网络节点,然后输入以下命令:
npm install request-promise --save 
  1. 安装这个request-promise库将允许我们向网络中的所有其他节点发出请求。一旦安装了该库,再次输入npm run node_1来重新启动第一个节点。

  2. 现在让我们去dev/networkNode.js文件,并将我们刚刚下载的库导入到代码中。在开头输入以下代码来导入库:

const rp = require('request-promise');

在上述代码行中,rp代表请求承诺。

  1. 现在让我们在register-and-broadcast-node端点中使用这个库。在这里,我们必须将我们的newNodeUrl广播到我们网络中的所有其他节点。使用我们刚刚导入的request-promise库来完成这个操作。

我们将要添加到代码中的下一些步骤可能看起来有点混乱,但不要担心。步骤完成后,我们将逐步走过代码,确保一切对您来说都是清晰的。现在让我们看看以下步骤:

  1. 我们的request-promise库的第一件事是定义我们将使用的一些选项,因此输入以下突出显示的代码行:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {

 }

}
  1. 在这个对象中,我们想要定义我们要为每个请求使用的选项。

  2. 我们要定义的第一个选项是我们要命中的 URI/URL。我们知道我们要命中所有其他networkNodeUrl上的register-node端点。因此,我们将在前面的代码块中添加以下突出显示的代码行:

bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
    uri: networkNodeUrl + '/register-node', 
    }

}
  1. 接下来,我们想定义我们要使用的方法。要命中register-node端点,我们将不得不使用POST方法,因此在前面的代码块中添加以下代码:
method: 'POST',
  1. 然后我们想知道我们将传递哪些数据,所以添加以下内容:
body: { newNodeUrl: newNodeUrl }
  1. 最后,我们要将json选项设置为 true,这样我们就可以将其作为 JSON 数据发送:
json: true
  1. 这些是我们要用于每个请求的选项。现在让我们看看如何使用这些选项。在requestOptions块之后,添加以下代码行:
rp(requestOptions)
  1. 上述请求将返回一个 promise 给我们,我们希望将所有这些 promise 放在一个数组中。因此,在forEach循环之前和之内,执行以下突出显示的更改:
const regNodesPromises = [];
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
        uri: networkNodeUrl + '/transaction',
        method: 'POST',
        body: newTransaction,
        json: true
    };
 regNodesPromises.push(rp(requestOptions));
});
  1. 现在在forEach循环之外,我们希望运行我们请求的所有 promise。在循环之后添加以下代码:
Promise.all(regNodesPromises)
.then(data => {
    //use the data...
});

继续在/register-and-broadcast-node 端点上工作

在这一部分,让我们继续构建我们的register-and-broadcast-node端点。到目前为止,我们已经在当前网络节点上注册了新节点,并且已经将新节点广播到我们网络中的所有其他节点。因此,我们正在访问我们网络中所有其他节点上的register-node端点。另外,目前我们假设那些其他节点正在注册新节点,虽然我们还没有构建它,但我们假设它正在工作。

在整个广播完成后,我们必须将目前在我们网络中的所有节点注册到我们正在添加到网络中的新节点。为此,我们将使用我们的request-promise库。因此,我们需要定义一些选项,如下面的代码中所突出显示的:

Promise.all(regNodesPromises)
.then(data => {
   const bulkRegisterOptions = { 
        uri: newNodeUrl + '/register-nodes-bulk'  
        method: 'POST',
 body: {allNetworkNodes: [...bitcoin.networkNodes,
        bitcoin.currentNodeUrl]} 
 json:true
 }; 
  });
});

在上述代码中,我们定义了要使用的选项(如uri)以及POST方法。在 body 选项中,我们定义了allNetworkNodes数组,并且在这个数组内,我们希望包含我们网络中所有节点的所有 URL,以及我们当前所在节点的 URL。此外,您可能已经注意到我们在数组中使用了扩展运算符...,因为bitcoin.networkNodes是一个数组,我们不希望一个数组嵌套在另一个数组中。相反,我们希望展开这个数组的所有元素并将它们放入我们的外部数组中。最后,我们希望将json定义为true

接下来,我们想要发出请求,因此在选项块之后,添加以下内容:

return rp(bulkRegisterOptions);

之后,添加以下内容:

.then (data => {

})

在上述代码行中的data变量实际上将是我们从上述 promise 中收到的数据。我们不打算对这些数据做任何处理,但我们想要使用.then,因为我们想在我们的端点内进行下一步操作。但是,我们只能在上述 promise 完成后才能这样做。

在这个端点内我们必须完成的最后一步是向调用它的人发送一个响应。因此,输入以下突出显示的代码行:

.then (data => {
    res.json({ note: 'New Node registered with network successfully' });
});

这就是我们的register-and-broadcast-node端点。

register-and-broadcast-node端点功能的快速回顾

现在让我们再次运行这个端点,以便快速总结我们在这个端点中所做的工作,以便更好地理解这一点。每当我们想要将新节点注册到我们的网络时,register-and-broadcast-node端点是我们想要访问的第一个点。在这个端点内我们要做的第一件事是获取newNodeUrl并将其通过将其推入我们的networkNodes数组中注册到当前节点。

我们接下来要做的一步是将这个newNodeUrl广播到我们网络中的其他节点。我们是在forEach循环内做这个操作。在这个循环内发生的一切就是我们向我们网络中的每个其他节点发出请求。我们正在向register-node端点发出这个请求。然后我们将所有这些请求推入我们的register-nodepromise 数组中,然后简单地运行所有这些请求。

一旦所有这些请求都完成且没有任何错误,我们可以假设newNodeUrl已成功注册到我们的所有其他网络节点。

广播完成后,我们要做的下一件事是将我们网络中已经存在的所有网络节点注册到我们的新节点上。为了做到这一点,我们向新节点发出单个请求,然后命中register-nodes-bulk端点。我们传递给这个端点的数据是我们网络中已经存在的所有节点的 URL。

然后我们运行rp(bulkRegisterOptions);,尽管我们还没有构建register-nodes-bulk端点,但我们假设它正在工作,并且我们的所有网络节点已经成功地注册到我们的新节点上。一旦发生这种情况,我们的所有计算就完成了,我们只需发送一条消息,说明新节点已成功注册到网络中。

在这一点上,这可能看起来很多,但不要担心;建议您继续前进。在接下来的部分,我们将构建我们的register-node端点,然后是我们的register-nodes-bulk端点。随着我们的操作,一切都会变得更清晰。

构建/register-node 端点

现在我们已经构建了/register-and-broadcast-node端点,是时候继续进行一些不那么复杂的事情了。在本节中,让我们开始构建register-node端点。与我们在上一节中构建的端点相比,这将非常简单。

这个register-node端点是网络中的每个节点都将接收到由我们的register-and-broadcast-node端点发送的广播。这个register-node端点唯一需要做的就是将新节点注册到接收到请求的节点上。

要开始构建register-node端点,请按照以下步骤进行:

  1. 我们要做的第一件事是定义newNodeUrl;因此,添加以下突出显示的代码行:
// register a node with the network
app.post('/register-node', function(req, res) {
       const newNodeUrl = req.body.newNodeUrl;
});

上一行代码只是简单地说明要使用发送到req.bodynewNodeUrl的值。这是我们发送到/register-node端点的数据,我们将把新的nodeNodeUrl保存为newNodeUrl变量。

  1. 接下来,我们要将newNodeUrl变量注册到接收到请求的节点上。为此,请添加以下突出显示的代码行:
// register a node with the network
app.post('/register-node', function(req, res) {
      const newNodeUrl = req.body.newNodeUrl; bitcoin.networkNodes.push(newNodeUrl);
});

上面的代码将我们的新节点注册到我们当前所在的节点。我们要做的就是将newNodeUrl简单地推送到当前节点的networkNodes数组中。

  1. 现在,我们要做的最后一件事就是发送一个响应,所以输入以下突出显示的代码行:
// register a node with the network
app.post('/register-node', function(req, res) {
      const newNodeUrl = req.body.newNodeUrl;bitcoin.networkNodes.push(newNodeUrl);
      res.json({ note: 'New node registered successfully.' }); 
});
  1. 接下来,我们要在这个端点内进行一些错误处理。我们唯一要做的就是,如果newNodeUrl在数组中不存在,就将其添加到我们的networkNodes数组中。为了做到这一点,我们将在bitcoin.networkNodes.push(newNodeUrl)的开头添加一个 if 语句。但在此之前,让我们定义一个变量,如下所示:
// register a node with the network
app.post('/register-node', function(req, res) {
      const newNodeUrl = req.body.newNodeUrl;
 const nodeNotAlreadyPresent = 
         bitcoin.networkNodes.indexOf(newNodeUrl) == -1; bitcoin.networkNodes.push(newNodeUrl);
       res.json({ note: 'New node registered successfully.' }); 
});

上面突出显示的行是在说明,如果newNodeUrl的索引是-1,或者换句话说,如果newNodeUrl在我们的网络节点中不存在,那么nodeNotAlreadyPresent变量将为 true。如果newNodeUrl已经存在于我们的networkNodes数组中,那么这个变量将为 false。

  1. 在 if 语句中,我们要说明的是,如果newNodeUrl不在我们的networkNodes数组中,则通过运行bitcoin.networkNodes.push(newNodeUrl)将其添加进去:
if (nodeNotAlreadyPresent ) bitcoin.networkNodes.push(newNodeUrl);
  1. 接下来,我们还要处理另一种情况,即如果newNodeUrl实际上是我们当前所在节点的 URL,我们不希望将newNodeUrl推送到我们的networkNodes数组中。为了在代码中提到这个条件,我们首先必须定义一个变量:
const notCurrentNode = bitcoin.currentNodeUrl !== newNodeUrl;

前面的一行只是评估bitcoin.currentNodeUrl !== newNodeUrl表达式,该表达式说明currentNodeUrl是否等于newNodeUrl。如果不是,则notCurrentNode变量将为 true。如果它们相等,则变量将为 false。

  1. 接下来,我们只需将notCurrentNode变量添加到我们的 if 语句中,如下所示:
if (nodeNotAlreadyPresent && notCurrentNode ) bitcoin.networkNodes.push(newNodeUrl);

这个 if 语句中发生的事情是,如果新节点不在我们的networkNodes数组中,并且新节点的 URL 与我们当前所在的节点不同,那么我们只想将新节点添加到我们的networkNodes数组中。

我们在端点内部进行错误处理。

测试/register-node 端点

在本节中,让我们测试/register-node端点,以确保它正常工作并更好地了解其工作原理。

安装请求库

在测试端点之前,我们需要进行一个小更新。更新涉及安装请求库。在几个部分之前,我们安装了request-promise库。现在,为了测试我们刚刚创建的端点,可能需要我们也安装请求库,这取决于我们安装的request-promise库的版本。

要安装请求库,只需转到终端,并在blockchain目录中运行以下命令:

npm install request --save

端点测试

在进行测试之前,请检查您的终端中是否有我们的五个网络节点都在运行。如果没有,那么您将需要设置它们。使用 Postman 测试register-node端点:

  1. 首先,我们将在地址栏中输入http://localhost:3001/register-node,如下截图所示:

当我们访问这个端点时,我们需要在req.body上发送newNodeUrl作为数据。我们现在需要设置它。因此,在 Postman 的 Body 选项卡中,我们希望选择原始和 JSON(application/json)作为文本。

  1. 然后,在文本框中,创建一个对象并添加以下代码:
{
    "newNodeUrl":""
}
  1. 现在假设我们要使用端口3002上运行的节点注册我们运行在端口3001上的节点。将以下内容添加到我们之前的代码中:
{
    "newNodeUrl":"http://localhost:3002"
}

到目前为止,我们已经使用运行在localhost:3002上的节点注册了我们运行在localhost:3001上的节点。因此,当我们访问http://localhost:3001/register-node时,我们的localhost:3002应该出现在第一个节点(即localhost:3001)的networkNodes数组中,因为这个register-node端点通过将节点放入networkNodes数组中来注册节点。

  1. 要验证这一点,打开 Postman 并单击发送按钮。您将收到响应“新节点成功注册”。现在转到浏览器,输入localhost:3001/blockchain到地址栏,然后按Enter。您将看到类似于以下截图所示的输出:

由于我们刚刚使用localhost:3001上的当前节点注册了我们的第二个节点,因此我们的第二个节点的 URL 现在在这个数组中。

按照相同的步骤,您也可以尝试注册其他节点。尝试进行实验。这将帮助您更清楚地了解已注册的节点。如果遇到任何问题,请尝试重新阅读整个过程。

我们要注意的一件重要的事情是,如果我们现在转到localhost:3002/blockchain,我们会发现networkNodes数组中没有注册的网络节点。

理想情况下,我们希望发生的是,当我们注册一个新节点时,我们希望它也进行反向注册。因此,如果我们使用3001上的节点注册localhost:3002,那么我们3002上的节点应该注册localhost:3001。这样,这两个节点都将彼此知晓。

实际上,我们已经在register-and-broadcast-node端点内构建了这个功能。一旦我们构建了这三个端点,我们提到的功能将正常工作。

构建/register-nodes-bulk 端点

我们要构建的下一个端点是我们的register-nodes-bulk端点;这是我们需要构建的最终端点。我们一直在处理的这三个端点将共同工作,创建我们的去中心化区块链网络。

在开始构建端点之前,让我们试着理解一下register-nodes-bulk端点的作用。每当一个新节点被广播到网络中的所有其他节点时,我们希望获取已经存在于网络中的所有节点,并将这些数据发送回我们的新节点,以便新节点可以注册和识别已经存在于网络中的所有节点。

register-nodes-bulk端点将接受包含已经存在于网络中的每个节点的 URL 的数据。然后,我们将简单地注册所有这些网络节点到新节点。

新节点是命中register-nodes-bulk端点的节点。这个端点只会在我们的网络中添加新节点时才会命中。

  1. 要构建register-nodes-bulk端点,我们将假设我们当前网络中的所有节点 URL 都作为数据传递,并且我们可以在req.body.allNetworkNodes属性上访问它们。这是因为在Promise.all(regNodesPromise)块中调用此端点时,我们正在发送allNetworkNodes数据。在这里,我们正在将allNetworkNodes发送到register-nodes-bulk端点。这将使我们能够在端点内部访问allNetworkNodes数据。

  2. 让我们在之前创建的register-nodes-bulk端点中添加以下代码行:

app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetowrkNodes;

});
  1. 接下来,让我们循环遍历allNetworkNodes数组中存在的每个节点 URL,并将其注册到新节点,如下所示:
app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetowrkNodes;
    allNetworkNodes.forEach(networkNodeUrl => { 
 //...
 });

});
  1. 现在,在循环中我们要做的就是将每个网络节点 URL 注册到我们当前所在的节点,也就是正在添加到网络中的新节点:
app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetowrkNodes;
    allNetworkNodes.forEach(networkNodeUrl => { 
        bitcoin.networkNodes.push(metworkNodeUrl);
    });

});

在上面突出显示的代码行中发生的情况是,当我们通过forEach循环遍历所有网络节点时,我们通过将networkNodeUrl推送到我们的networkNodes数组中来注册每一个节点。

每当我们命中/register-nodes-bulk端点时,我们都在添加到网络中的新节点上。所有这些networkNodeUrls都将被注册到我们正在添加的新节点上。

  1. 现在有几种情况下,我们不希望将networkNodeUrl添加到我们的networkNodes数组中。为了处理这些情况,我们将使用一个 if 语句。但在此之前,我们需要定义一个条件语句,如下所示:
const nodeNotAlreadyPresent = bitcoin.networkNodes.indexOf(networkNodeUrl) == -1;

如果networkNodeUrl已经存在于networkNodes数组中,我们就不希望将其添加到networkNodes数组中;这就是我们在条件语句中提到的。

这个语句所做的就是测试我们当前所在的networkNodeUrl是否存在于我们的networkNodes数组中。从这里,它将简单地将其评估为真或假。

  1. 现在我们可以添加nodeNotAlreadyPresent变量和 if 语句,如下面的代码中所突出显示的那样:
app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetowrkNodes;
    allNetworkNodes.forEach(networkNodeUrl => {
    const nodeNotAlreadyPresent = 
      bitcoin.networkNodes.indexOf(networkNodeUrl) == -1; 
        if(nodeNotAlreadyPresent)bitcoin.networkNodes.push(networkNodeUrl);
 });

});

上面的 if 语句说明,如果节点尚未存在于我们的networkNodes数组中,那么我们将注册该节点。

  1. 现在,另一种情况是,如果要注册的网络节点具有与我们当前所在的网络节点相同的 URL,我们就不希望注册该网络节点。为了处理这个情况,我们需要另一个变量:
const notCurrentNode = bitcoin.currentNodeUrl !==networkNodeUrl
  1. 接下来,将这个变量添加到我们的if语句中:
app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetowrkNodes;
    allNetworkNodes.forEach(networkNodeUrl => {
    const nodeNotAlreadyPresent = 
      bitcoin.networkNodes.indexOf(networkNodeUrl) == -1; 
        if(nodeNotAlreadyPresent && notCurrentNode)
         bitcoin.networkNodes.push(networkNodeUrl);
 });

});

基本上,在if语句中我们所陈述的是,当我们循环遍历每个要添加的网络节点时,如果该节点尚未存在于我们的网络节点数组中,并且该节点不是我们当前节点的 URL,那么我们就要将networkNodeUrl添加到我们的networkNodes数组中。

  1. 完成forEach循环后,我们将注册所有已经存在于我们区块链网络中的网络节点。在这一点上,我们所要做的就是发送回一个响应,如下所示:
app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetowrkNodes;
    allNetworkNodes.forEach(networkNodeUrl => {
    const nodeNotAlreadyPresent = 
      bitcoin.networkNodes.indexOf(networkNodeUrl) == -1; 
        if(nodeNotAlreadyPresent && notCurrentNode)
         bitcoin.networkNodes.push(networkNodeUrl);
 });
res.json({note: 'Bulk registration successful.' });

});

让我们快速回顾一下我们到目前为止所做的工作。我们构建的端点接受所有网络节点作为数据,然后我们循环遍历已经存在于我们区块链网络中的所有网络节点。对于每个节点,只要它尚未注册到currentNode并且不是与currentNode相同的 URL,我们就会将该节点添加到我们的networkNodes数组中。

测试/register-nodes-bulk 端点

在这一部分,我们将测试我们的register-nodes-bulk端点,以确保它正常工作。这将使我们清楚地了解它的工作原理:

  1. 为了测试这个端点,我们将前往 Postman。在这里,我们将命中localhost:3001/register-nodes-bulk端点。当我们测试这个端点时,我们期望收到一些数据,即allNetworkNodes数组。

  2. 因此,在 Postman 的 body 选项卡中,选择原始选项和 JSON(application/json)格式,将以下代码添加到 body 中:

{
    "allNetworkNodes": []
}
  1. 在这个数组中,将包含已经存在于我们区块链网络中的所有节点的 URL:
{
    "allNetworkNodes": [
    "http://localhost:3002",
    "http://localhost:3003",
    "http://localhost:3004"
    ]
}
  1. 当我们现在运行这个请求时,我们应该在运行在localhost:3001上的节点上注册这三个 URL。让我们看看是否有效。点击发送按钮,您将收到一个回复,说明批量注册成功。

  2. 现在,如果我们转到浏览器,我们可以双重检查它是否有效。在地址栏中,键入localhost:3001/blockchain,然后按Enter。您将看到networkNodes数组中添加的三个 URL,因为它们是批量注册的:

同样,您可以尝试通过将新节点添加到不同 URL 上的其他节点来进行实验。您将观察到这些节点的networkNodes数组中的类似响应。

因此,看起来我们的register-node-bulk端点正在按照预期工作。

测试所有网络端点

根据我们在前面部分学到的知识,我们知道我们的register-node路由和register-nodes-bulk路由都正常工作。因此,在本节中,让我们把它们全部整合起来,测试我们的register-and-broadcast-node路由,该路由同时使用了register-node路由和register-nodes-bulk路由。

register-and-broadcast-node端点将允许我们通过创建网络并向其添加新节点来构建分散的区块链网络。让我们立即进入我们的第一个示例,以更好地理解它。为了理解register-and-broadcast-node路由的工作原理,我们将使用 Postman。

在 Postman 应用程序中,我们要发出一个 post 请求,以在localhost:3001上注册和广播节点。但在这之前,只需确保所有四个节点都在运行,以便我们可以测试路由。

此时,我们根本没有网络;我们只有五个独立的节点在运行,但它们没有以任何方式连接。因此,我们将要做的第一个调用只是简单地将两个节点连接在一起,以形成我们网络的开端。我们现在将一个节点注册到我们在端口3001上托管的节点。当我们命中register-and-broadcast-node端点时,我们必须发送一个要注册的newNodeUrl。在 Postman 中,添加以下代码:

{
    "newNodeUrl": ""
}

对于这个第一次测试,我们想要将我们托管在端口3002上的第二个节点注册到我们的第一个节点。为此,我们将添加以下突出显示的代码:

{
    "newNodeUrl": "http://localhost:3002"
}

现在,当我们发出这个请求时,它应该将我们托管在localhost:3002上的节点注册到我们托管在localhost:3001上的节点。让我们通过单击“发送”按钮来验证这一点。您将看到类似于以下屏幕截图中显示的输出:

从前面的屏幕截图中,我们可以看到新节点已成功注册到网络。让我们通过转到浏览器来验证这一点。

在浏览器中,您将可以访问所有正在运行的五个节点。我们现在已经将端口3002上的节点注册到了托管在localhost:3001上的节点。因此,如果我们现在在浏览器上刷新页面,我们将看到localhost:3002已经在端口3001networkNodes数组中注册了:

从前面的屏幕截图中,我们可以看到我们已经注册了localhost:3002。现在,如果我们转到localhost:3002,我们应该在它的networkNodes数组中有localhost:3001注册。让我们刷新并看看我们在这里得到了什么:

从前面的屏幕截图中,我们可以看到两个节点现在已经形成了一个网络,并将彼此注册为网络节点。

接下来,让我们向这个网络添加另一个节点。让我们回到 Postman,并将localhost:3002更改为localhost:3003。我们将向在3001上的节点发出请求:

{
    "newNodeUrl": "http://localhost:3003"
}

这应该是将我们托管在localhost:3003上的节点与网络中的所有其他节点注册。因此,3003应该注册到30013002。让我们发送这个请求,看看它是否成功注册。如果成功注册,您将看到类似于以下屏幕截图中显示的输出:

让我们在浏览器中验证这一点。当我们在localhost:3001中刷新时,我们应该在networkNodes数组中有localhost:3003

现在,由于localhost:3002也是网络的一部分,它的networkNodes数组中应该有localhost:3003。当我们发出这个请求时,我们是发给3001而不是3002localhost:3002已经是网络的一部分,广播注册了3003与网络中存在的所有网络节点。要验证这一点,请刷新3002上的networkNodes数组。您将看到类似于以下屏幕截图中显示的输出:

从前面的屏幕截图中,我们可以看到我们的第三个节点现在也在localhost:3002networkNodes数组中。此外,如果我们转到localhost:3003上的networkNodes并刷新页面,我们应该在networkNodes数组中有30013002

因此,我们现在有一个由300130023003节点组成的网络。这些节点已经相互注册。

现在,让我们回到 Postman,并按照注册初始节点的相同步骤,将剩下的localhost:3004localhost:3005注册到网络中。

在将30043005注册到网络后,如果您转到浏览器,所有这些注册节点应该在它们的networkNodes数组中包含localhost:3004localhost:3005。刷新localhost:3001页面,您将看到类似于以下屏幕截图中显示的输出:

同样地,如果您刷新其他页面,您将能够观察到所有节点,类似于我们在前面的屏幕截图中观察到的。

这就是我们建立了一个由五个不同节点组成的去中心化网络。

现在,您可能想知道所有这些是如何工作的。它之所以能够工作,是因为当我们发出"newNodeUrl": "http://localhost:3004"的请求时,我们实际上是在添加一个命令,将3004添加到网络中。但是localhost:3004如何在一次请求中意识到整个网络呢?

如果您还记得前面的部分,当我们构建/register-and-broadcast-node端点时,实际上进行了大量的计算。因此,如果我们看一下/register-and-broadcast-node端点的代码,我们可以看到我们的register-and-broadcast-node端点内部发生的第一件事是接收newNodeUrl,然后通过访问它们的register-node端点将其广播到网络中的每个节点。因此,网络中的每个节点都将意识到新添加的节点。

有关完整的代码,请访问github.com/PacktPublishing/Learn-Blockchain-Programming-with-JavaScript/blob/master/dev/networkNode.js,并参考以此注释开头的代码块://registering a node and broadcasting it the network

然后,在广播发生后,我们向刚刚添加的新节点发送请求,并使用新节点注册网络中已经存在的所有网络节点。这就是反向注册发生的地方。在这一点上,网络中的所有原始节点都意识到了新节点,而新节点也意识到了网络中的所有其他节点。因此,网络中的所有节点都意识到了彼此,这是我们的区块链正常工作所必须发生的事情。

因此,我们构建的这三个端点(register-and-broadcast-noderegister-noderegister-nodes-bulk)非常强大,因为它们共同工作以创建一个分散的区块链网络。这就是我们在本章中构建的内容。

在本书的这一部分,建议您花一些时间玩弄这些端点,创建不同的具有不同节点的网络,并进行一些测试,以更熟悉它的工作原理。

如果您对我们所涵盖的任何概念或主题感到困惑,建议您再次阅读本章的所有部分。您会惊讶地发现,在您已经对即将发生的事情和我们将要构建的内容有一些背景之后,第二次阅读时您可以学到多少东西。

总结

我们现在已经完成了创建我们的分散网络。在本章中,我们学习了许多新概念。我们开始学习如何创建我们 API 的多个实例以及如何使用它们来设置我们的分散网络。然后,我们定义了各种端点,如register-and-broadcast-noderegister-noderegister-nodes-bulk。之后,我们构建了这些端点并对其进行了测试。

在下一章中,我们将学习如何同步网络。

第五章:同步网络

在之前的章节中,我们构建了一个由五个节点组成的网络。每个节点都知道网络中的所有其他节点,这创建了一个去中心化的区块链网络。现在我们需要创建一个同步的网络,以便每个节点上的区块链都是相同的,数据在整个网络中是一致的。我们不能容忍在不同节点上运行不同版本的区块链,因为这将完全破坏区块链的目的。应该只有一个版本的区块链在每个节点上是一致的。因此,在本章中,让我们同步在第四章中构建的网络,创建一个去中心化的区块链网络。我们将通过在网络中的所有节点之间广播已挖掘的交易和新区块来实现这一点。

在本章中,将涵盖以下主题:

  • 理解同步网络的需求

  • 构建/transaction/broadcast 端点

  • 重构createTransaction方法和/transaction端点

  • 测试交易端点

  • 更新挖矿信息

  • 构建/receive-new-block 端点

  • 测试新的和更新的/mine 端点

让我们开始同步网络。

理解同步网络的需求

让我们试着理解为什么网络需要同步。我们目前有一个由五个节点组成的去中心化区块链网络。这些节点之间的数据不一致;每个节点上的数据可能不同,这将导致区块链的目的失败。让我们通过一个例子来理解这种情况。在 Postman 中发送一个示例交易,如下截图所示:

通过单击“发送”按钮将此交易发送到托管在localhost:3001上的节点。此交易将出现在localhost:3001/blockchainpendingTransactions数组中,您可以在以下截图中观察到:

现在,转到任何其他节点并检查发送的交易。我们将无法在这些节点的pendingTransactions数组中查看交易。发送的示例交易只会出现在localhost:3001节点中。它不会广播到网络中的任何其他节点。

在本章中,您要做的是重构/transaction 端点,以便每当创建交易时,它都会广播到所有节点。这意味着所有节点将具有相同的数据。我们需要做同样的事情来挖掘一个区块。让我们重构/mine 端点,以便每当挖掘出一个新块时,它也会广播到整个网络。这意味着整个网络是同步的,并且具有相同数量的区块。通过网络同步数据是区块链技术的一个重要特性。

重构 createNewTransaction 方法和/transaction 端点

在本节中,让我们通过将createNewTransaction方法拆分为两个独立的部分来重构。一部分将简单地创建一个新交易,然后返回该交易,另一部分将把新交易推送到pendingTransactions数组中。我们还将创建一个名为/transaction/broadcast的新交易端点。此端点将允许我们在整个区块链网络中广播交易,以便每个节点具有相同的数据,并且整个网络是同步的。

修改 createNewTransaction 方法

在这里,让我们将createNewTransaction方法拆分为两个独立的方法,修改如下:

  1. 转到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;
}
  1. 让我们对该方法进行以下突出显示的修改:
Blockchain.prototype.createNewTransaction = function (amount, sender, recipient) {
    const newTransaction = {
        amount: amount,
        sender: sender,
        recipient: recipient,
        transactionId: uuid().split('-').join('')
    };
    return newTransaction;
}

在这里,为每个交易添加了一个 ID。为了创建这个 ID,使用了一个唯一的字符串,这与我们在第三章中用于创建节点地址的方法非常相似,通过 API 访问区块链

  1. 使用uuid库创建 ID 的唯一字符串。因此,在dev/blockchain.js文件的开头,定义所有常量的地方,您需要添加以下代码行,以便在我们的项目中使用uuid库:
const uuid = require('uuid/v1');

在修改后的方法中,您可以观察到添加了以下代码行,以为transactionId值创建唯一的字符串。这是实现uuid库的地方:

transactionId: uuid().split('-').join('')

在这里,.split()函数将去除添加到唯一字符串的破折号,然后.join()函数将重新连接字符串,以输出每个交易的唯一Id

构建 addTransactionToPendingTransactions 方法

接下来,我们需要将返回的newTransaction推送到区块链的pendingTransactions数组中。因此,让我们创建另一个名为addTransactionToPendingTransactions的方法:

  1. dev/blockchain.js文件中,addTransactionToPendingTransactions方法将定义如下:
Blockchain.prototype.addTransactionToPendingTransactions = function(transactionObj) {
};
  1. 接下来,获取transactionObj并将其推送到区块链的pendingTransactions数组中:
Blockchain.prototype.addTransactionToPendingTransaction = function(transactionObj) {
    this.pendingTransactions.push(transactionObj);

};
  1. 然后,我们只需返回添加了交易的区块的索引:
Blockchain.prototype.addTransactionToPendingTransaction = function(transactionObj) {
    this.pendingTransaction.push(transactionObj);
    return this.getLastBlock()['index'] + 1;
};

简而言之,我们修改了createNewTransaction方法,该方法创建一个新的交易,并返回该新交易。然后,我们创建了一个名为addTransactionToPendingTransactions的新方法。该方法接受一个transactionObj并将其添加到区块链上的pendingTransactions数组中。之后,我们只需返回添加了新交易的区块的索引。

构建/transaction/broadcast 端点

在本节中,让我们构建一个名为/transaction/broadcast的新端点。从现在开始,每当我们想要创建一个新的交易时,我们将访问此/transaction/broadcast端点。此端点将执行两项操作:

  • 它将创建一个新的交易。

  • 然后,它将向网络中的所有其他节点广播该新交易。

让我们按以下步骤创建端点:

  1. 要添加此端点,请转到dev/networkNode.js文件,我们在其中定义了所有端点,并按以下方式添加新端点:
app.post('/transaction/broadcast', function(req, res) )  {

});
  1. 然后,为了使端点执行上述功能,将以下突出显示的代码添加到端点:
app.post('/transaction/broadcast', function(req, res) )  {
    const newTransaction = bitcoin.createNewTransaction();

});

这里的createNewTransaction()方法是上一节中修改过的方法。

  1. createNewTransaction()方法接受amountsenderrecipient参数。对于我们的端点,让我们假设所有这些数据都被发送到req.body上。因此,这些参数将如下所示在以下代码中进行定义:
app.post('/transaction/broadcast', function(req, res) )  {
    const newTransaction = bitcoin.createNewTransaction(req.body.amount, req.body.sender, req.body.recipient);

});
  1. 接下来,让我们借助addTransactionToPendingTransactions方法将newTransaction变量添加到节点的pendingTransactions数组中。因此,在前面的代码行之后,添加以下行:
bitcoin.addTransactionToPendingTransactions (newTransaction);
  1. 现在,将新交易广播到网络中的所有其他节点。可以按以下方式完成:
bitcoin.netowrkNodes.forEach(networkNodeUrl => {
    //...
});
  1. 在这个forEach循环中,让我们定义广播交易的代码。为此,向网络中的所有其他节点的/transaction端点发出请求。因此,在循环内,添加以下行:
const requestOptions = {

};
  1. 然后,定义我们所有的选项,如下所示:
const requestOptions = {
    uri: networkNodeUrl + '/transaction',
 method: 'POST',
 body: newTransaction,
 json: true
};
  1. 接下来,让我们创建一个承诺数组,将所有请求推送到该数组中,以便我们可以同时运行所有请求。让我们在forEach循环之前定义数组如下:
const requestPromises = []; 
  1. 然后,在定义所有选项之后,进行请求如下:
requestPromises.push(rp(requestOptions));

在这行代码之前,我们将把所有请求推送到requestPromises数组中。forEach循环运行后,我们应该在requestPromises数组中有所有我们定义的请求。

  1. 接下来,让我们运行所有请求。在forEach循环之后,添加以下行:
promise.all(requestPromises)
  1. 最后,在所有请求运行后,我们将添加以下行:
.then(data => {

});
  1. 我们实际上不会使用所有这些请求返回的数据,但我们会发送一个响应,因为在这一点上,整个广播已经完成。因此,在上述代码块中,添加以下突出显示的代码:
.then(data => {
    res.json({ note: 'Transaction created and broadcast successfully.'})
});

通过添加上述代码行,我们已成功完成了构建/transaction/broadcast端点。

重构/transaction 端点

在本节中,我们将重构/transaction端点,以便它可以与新的/transaction/broadcast端点完美配合。让我们应用以下步骤修改端点:

  1. 首先,转到dev/networkNode.js文件,并删除/transaction端点中的所有内容。只有在进行广播时,才会访问/transaction端点。当访问/transaction端点时,newTransaction变量将作为数据发送。可以定义如下条件:
app.post('/transaction', function(req, res) {
    const newTransaction = req.body;

};

在上面突出显示的行中,newTransaction变量通过req.body发送到/transaction端点。

  1. 接下来,将新交易添加到接收调用的任何节点的pendingTransactions数组中。为此,将使用新的addTransactionToPendingTransactions方法。因此,在上述代码的后面,添加以下行:
bitcoin.addTransactionToPendingTransactions();
  1. 这个方法简单地接收newTransaction变量:
bitcoin.addTransactionToPendingTransactions(newTransaction);
  1. 现在,从addTransactionToPendingTransactions方法中,我们得到交易将被添加到的块的索引。让我们在新的/transaction端点中保存这个块索引。在上述代码的开始处,添加变量如下:
const blockIndex = bitcoin.addTransactionToPendingTransactions(newTransaction);
  1. 最后要做的是发送一个响应。在上述行之后,添加以下内容:
res.json({ note: 'Transaction will be added in block ${blockIndex}.'});

我们现在已经完成了对/transaction端点的重构。

测试交易端点

让我们测试/transaction/broadcast/transaction端点,确保它们能够正确配合工作。

对于这个测试,我们需要做的第一件事是将所有节点连接在一起,以构建一个网络。您可能还记得如何做到这一点,因为我们在第四章中学习过,创建分散的区块链网络。无论如何,我们将快速浏览一遍这些步骤,以便您记起来。

回顾如何创建网络

看一下以下步骤,了解如何连接所有节点:

  1. 打开 Postman 并访问/register-and-broadcast-node路由。这可以在任何一个节点上完成。在我们的示例中,让我们使用localhost:3001

  2. 现在,在正文中,我们要通过传递其 URL 来向我们的网络添加一个新节点。让我们从第二个节点开始。看一下以下的截图:

  1. 然后,点击发送按钮发送请求。发送请求后,您将收到一个响应,上面写着“新节点已成功注册到网络”。您可以以相同的方式发送所有剩余的节点。

  2. 要验证所有节点是否正确连接以形成网络,请转到浏览器,输入localhost:3001/blockchain在地址栏中,然后按Enter。您将在networkNodes数组中看到所有节点。

测试交易端点

现在区块链网络已经建立,让我们测试一下我们在之前部分创建的端点。

让我们创建一个交易并将其发送到/transaction/broadcast端点。返回到 Postman,命中端口为3001的节点的/transaction/broadcast端点。在这里,发送一些数据作为交易,如下面的屏幕截图所示:

您发送的交易数据可以是任意随机数据。我们只需要金额、发送方和接收方。一旦添加了交易数据,让我们点击发送按钮发送此请求。如果交易成功发送,将收到一个响应,上面写着“交易已成功创建和广播”。

现在,转到浏览器,您应该能够在网络的每个节点上看到我们创建的交易。让我们检查一下这是否有效。在浏览器的地址栏中,输入localhost:3001/blockchain,然后按Enter。您应该看到pendingTransactions数组中的交易数据,如下面的屏幕截图所示:

在这里,pendingTransactions数组中的交易现在也有一个以随机哈希开头的transactionId值。

接下来,打开另一个标签页,输入localhost:3002/blockchain在地址栏中,然后按Enter。您可以看到相同的交易数据可以在数组中看到:

如果您转到网络中的其他节点,您可以对所有剩余节点进行类似的检查。您可以观察到每个节点的pendingTransactions数组中的相同交易数据。区块链网络中的每个节点现在都知道已创建新交易。

您也可以尝试使用其他交易数据测试端点。尝试将金额更改为500,将发送方和接收方的地址更改为随机哈希字符串,并尝试将此请求发送到托管在localhost:3004上的节点。这不会有任何影响,因为广播端点将交易数据发送到网络中的所有节点。因此,这个请求应该像上一个一样工作。在浏览器上检查响应,您应该能够看到两个具有不同交易 ID 的交易。

尝试使用不同的交易数据进行实验,以清楚了解/transaction/transaction/broadcast端点的工作原理。

从测试中,我们可以得出结论,/transaction/broadcast端点和/transaction端点都按我们预期的那样正常工作。

在下一节中,我们将通过重构/mine端点来继续同步网络,以便它将新创建的新块广播到整个网络。

更新挖矿信息

同步网络所需的下一步是更新/mine端点。我们还将添加一个新的端点,称为/receive-new-block。有必要更新/mine端点,以便每当一个节点创建一个新块时,该新块被广播到网络中的所有其他节点。这意味着网络中的每个节点都知道已创建新块,并且托管区块链的所有节点保持同步。

更新后的挖矿流程

每当挖掘出一个新块时,它将在特定节点上被挖掘。为了理解更新后的挖矿流程,让我们假设我们希望一个托管在端口3001上的节点为区块链挖掘一个新块:

  1. 首先,将在所选节点上命中/mine端点。当命中/mine端点时,通过工作证明创建一个新块。

  2. 新块创建后,它将被广播到网络中的所有其他节点。所有其他节点将在其/receive-new-block端点接收到该新块。如下图所示:

  1. 广播完成后,整个网络将同步,并且所有节点将托管相同的区块链。

另一件事需要注意的是,当新区块被广播并且节点接收到它时,该新区块将在链验证该区块合法后被添加到链中。然后,节点清除其pendingTransactions数组,因为所有待处理交易现在都在它们刚刚收到的新区块中。

在接下来的几节中,我们将逐步构建整个过程。随着我们构建每个步骤,应该更容易看到所有内容是如何协同工作的。

重构/mine端点

通过实施以下步骤来重构/mine端点:

  1. 转到dev/networkNode.js文件。在/mine端点中,在我们定义了newBlock变量的部分下面,让我们添加将新区块广播到网络中所有其他节点的功能。为此,请按照我们在前几节中介绍的相同过程进行,即循环遍历网络中的所有其他节点,向节点发出请求,并将newBlock变量作为数据发送:
bitcoin.networkNodes.forEach(networkNodeUrl => {

})

前面的一行提到,对于每个networkNodes,我们将发出请求并发送newBlock

  1. 然后,我们需要发送一些请求选项。这些选项将定义如下:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {

 }; 

})
  1. 该对象中的第一个选项是uri。我们要发送请求的uri将是networkNodeUrl和我们将要创建的新端点,即/receive-new-block。我们将在下一节中处理此端点:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
        uri: networkNodeUrl + '/receive-new-block',   
    }; 

})
  1. 要添加的下一个选项是将使用的方法,即POST方法:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
        uri: networkNodeUrl + '/receive-new-block', method: 'POST',   
    }; 

})
  1. 接下来,让我们发送将在body中的数据。我们还想发送一个newBlock实例:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
        uri: networkNodeUrl + '/receive-new-block',method: 'POST',        body: { newBlock: newBlock }
    }; 

})
  1. 最后,在body之后,将json设置为true,如下所示:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
        uri: networkNodeUrl + '/receive-new-block',method: 'POST',       body: { newBlock: newBlock },
        json: true
    }; 

})
  1. 之后,通过添加以下突出显示的代码,进行请求:
bitcoin.networkNodes.forEach(networkNodeUrl => {
    const requestOptions = {
        uri: networkNodeUrl + '/receive-new-block',method: 'POST',       body: { newBlock: newBlock },
       json: true
    }; 
    rp(requestOptions)
})
  1. 每次进行这些请求时,它都会返回一个 promise。通过添加以下突出显示的代码,让我们创建所有这些 promises 的数组:
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循环运行后,我们应该有一个充满了 promises 的数组。

  1. 接下来,让我们运行所有这些 promises。因此,在forEach块之后,添加以下代码:
Promise.all(requestPromises)
.then(data => {
    // ....
})

所有请求运行后,我们希望在.then(data => { })内执行另一个计算。如果记得,当创建新交易时,挖矿奖励交易代码bitcoin.createNewTransaction(12.5, "00", nodeAddress);需要在整个区块链网络中广播。目前,当挖掘出新区块时,我们创建了一个挖矿奖励交易,但它没有广播到整个网络。为了广播它,请求将被发送到/transaction/broadcast端点,因为它已经具有广播交易的功能。我们只需使用传递的挖矿奖励交易数据调用此端点。

  1. 然而,在传递挖矿奖励交易数据之前,我们需要一些请求选项:
Promise.all(requestPromises)
.then(data => {
    const requestOptions = {
 uri: bitcoin.currentNodeUrl + '/transaction/broadcast',
 method: 'POST',
    };    

})
  1. body数据将作为对象发送。在body中,让我们添加挖矿奖励交易数据:
Promise.all(requestPromises)
.then(data => {
    const requestOptions = {
        uri: bitcoin.currentNodeUrl + '/transaction/broadcast',
        method: 'POST',
        body: {
 amount: 12.5, 
 sender:"00", 
 recipient: nodeAddress
 }
    };    

})
  1. 最后,在body之后,通过添加以下行将json设置为true
json: true
  1. 然后,在requestOptions之后,让我们发送以下请求:
return rp(requestOptions);

/mine端点内部,正在进行一系列计算以创建新的区块。然后,一旦创建了新的区块,它将被广播到网络中的所有其他节点。广播完成后,在.then块内,将发出对/transaction/broadcast端点的新请求。此请求将创建一个挖矿奖励交易,然后节点将其广播到整个区块链网络。然后,在请求运行并完成所有计算后,将发送响应:成功挖掘新区块。

您可以在github.com/PacktPublishing/Learn-Blockchain-Programming-with-JavaScript/blob/master/dev/networkNode.js上查看完整更新的 mine 端点代码。

构建/receive-new-block 端点

接下来要做的是构建我们在更新的/mine 端点中使用的/receive-new-block 端点。让我们开始构建这个端点:

  1. dev/networkNode.js文件中,在/register-and-broadcast-node端点之前,定义/receive-new-block端点如下:
app.post('/receive-new-block', function(req, res) {
};
  1. 在此端点内,代码期望接收正在广播的新区块。让我们将新区块保存在一个变量中,如下面的代码所示:
app.post('/receive-new-block', function(req, res) {
    const newBlock = req.body.newBlock;

};
  1. 当所有其他节点接收到这个新区块时,它们需要检查它是否真的是一个真实的区块,并且是否正确地适应了链。为了验证这一点,检查newBlock上的previousBlockHash,以确保它等于链中最后一个区块上的哈希。为此,需要访问链中的最后一个区块:
app.post('/receive-new-block', function(req, res) {
    const newBlock = req.body.newBlock;
   const lastBlock = bitcoin.getLastBlock(); 
};
  1. 接下来,让我们测试链中最后一个区块的哈希是否等于newBlock实例中的previousBlockHash
  lastBlock.hash === newBlock.previousBlockHash; 
  1. 这样,我们知道这个newBlock确实紧跟在链中的lastBlock之后。定义的前面语句将返回truefalsetruefalse值将保存在correctHash变量中:
const correctHash = lastBlock.hash === newBlock.previousBlockHash;
  1. 在进行上述检查之后,我们还希望确保newBlock具有正确的索引。这意味着newBlock的索引应该比链中的lastBlock高一个。添加以下检查:
const correctIndex = lastBlock['index'] + 1 === newBlock['index'];
  1. 接下来,根据newBlock是否合法需要采取两种不同的行动。如果newBlock是合法的,应该被接受并添加到链中。如果不合法,应该被拒绝。为了定义这个条件,让我们使用一个if-else语句:
if (correctHash && correctIndex) {
    bitcoin.chain.push(newBlock);

}
  1. 现在,由于newBlock已经被添加到链中,pendingTransactions数组需要被清空,因为待处理的交易现在已经在新区块中。因此,在if语句中,需要添加下一个条件如下:
bitcoin.pendingTransaction = [];
  1. 接下来,需要做的最后一件事是发送一个响应,表示该区块已被接受并添加到链中。在if语句中,在前面的行下面,添加以下响应:
res.json({
    note: 'New block received and accepted.',
    newBlock: newBlock
})
  1. 如果newBlock不合法并且未通过先前定义的任何测试,则在else语句中发送响应以指示该区块已被拒绝:
else{
  res.json({
      note:'New block rejected.',
      newBlock: newBlock
  });  
}

通过添加上述条件,我们已经完成了/receive-new-block 端点的构建。

测试新的和更新的/mine 端点

让我们测试更新的/mine 端点和我们刚刚创建的/receive-new-block 端点。基本上,/mine 端点将为我们挖掘新的区块。它还将获取该区块并将其广播到整个区块链网络,以便每个节点都同步,并且所有节点都具有相同的区块和相同的数据。这是我们在测试/mine 端点时期望观察到的结果:

  1. 要开始,您应该让所有五个节点都在运行。您还应该将它们连接在一起,以创建一个区块链网络。

  2. 接下来,转到浏览器。这里要做的第一件事是选择一个节点来挖掘新的区块。我们有五个节点可供选择,但在我们的情况下,我们将坚持使用第一个节点。因此,在地址栏中键入localhost:3001/mine,然后按Enter。您将得到以下输出:

矿端点似乎已经完美地工作了。响应表明新区块已经被成功挖掘和广播。您还可以在前面的屏幕截图中看到新的区块及其索引。

  1. 让我们验证新区块是否已添加到网络中。首先,在第一个节点上进行验证。在浏览器中打开另一个标签页,输入localhost:3001/blockchain,然后按Enter。您可以看到新区块已添加到网络中,如下所示:

在上述截图中,您可能还注意到pendingTransactions数组中存在一些交易。这些待处理交易实际上是我们刚刚挖掘的区块的挖矿奖励。更新的/mine端点定义了在创建新区块后应广播挖矿奖励交易。

从现在开始,每当创建新区块时,该区块的挖矿奖励将进入pendingTransactions数组,并将添加到下一个区块中。这就是比特币区块链中挖矿奖励的工作原理。在前两章中创建区块链时,我们将挖矿奖励直接放入了我们挖掘的区块中。现在区块链更加先进,我们拥有了一个去中心化的网络,遵循最佳实践并将挖矿奖励放入下一个区块对我们来说非常重要。

让我们回到/mine端点并继续测试。让我们检查网络内的其他节点,并验证挖掘的新区块是否已添加到这些节点中。此外,让我们检查生成的挖矿奖励是否也已广播到网络中的其他节点。

在浏览器中打开另一个标签页,输入localhost:3002/blockchain,然后按Enter。您将看到以下输出:

在上述截图中,您可以看到端口为3002的节点接收到了新挖掘的区块,以及挖矿奖励交易。您可以验证网络中其余节点的情况。

现在让我们从另一个节点挖掘另一个区块。不要转到localhost:3001,而是在浏览器的地址栏中输入localhost:3004/mine,然后按Enter。将挖掘新的区块;输出将如下所示:

从上述截图中,您可以观察到这是第三个区块。这是正确的,因为我们已经挖掘了两个区块。在区块的transactions数组中,您可以看到我们从上一个区块获得的挖矿奖励。这笔交易是端口为3001的节点在挖掘上一个区块时生成的挖矿奖励。

让我们转到localhost:3001/blockchain,验证我们刚刚挖掘的新区块是否已添加到网络中。您将看到以下响应:

在此截图中,您可以观察到刚刚挖掘的新区块已添加到端口为3001的节点中。该区块的交易数组包括来自上一个区块的挖矿奖励。我们现在在pendingTransactions数组中也有一个新的挖矿奖励,这是在挖掘第三个区块时生成的。通过之前使用的类似验证过程,您可以检查我们挖掘的第三个区块是否已添加到所有剩余节点中。

从这些测试中,看起来/mine端点正在按照预期工作。它正在创建新区块并将其广播到整个网络。这意味着整个网络是同步的,并且具有完全相同的区块链数据,这对于区块链正常工作非常重要。

让我们进一步测试端点。转到 Postman,创建一些交易,然后广播它们。之后,让我们挖掘一个新的区块,以查看新交易是否已正确添加到区块链中:

  1. 现在转到您的 Postman 并创建以下交易:

  1. 接下来,为了广播交易,请访问/transaction/broadcast端点。您可以将此交易数据发送到任何节点,并且应该会广播到整个网络。在我们的示例中,让我们将此交易发送到端口3002上的节点:

  1. 现在,点击发送按钮。然后,您将收到响应,表示交易已成功创建和广播。

您也可以尝试进行其他交易,就像我们之前所做的那样,通过更改金额值和发送方和接收方的地址。另一个测试是将交易数据发送到不同的节点。

  1. 现在,让我们返回浏览器,检查节点,以验证它们是否都收到了我们刚刚创建的交易。因为我们之前在浏览器中加载了节点3001,让我们刷新它。您应该会得到以下输出:

从前面的屏幕截图中,您可以观察到该节点有我们创建的所有三笔交易,以及上一个区块中的挖矿奖励,都在pendingTransactions数组中。同样,您可以验证其他节点的pendingTransaction数组。因此,我们可以得出结论,我们创建的所有交易都被完美地广播到整个网络。

现在,让我们挖掘一个新的区块,以验证所有待处理的交易是否已添加到新的区块中。在本例中,让我们在3003节点上挖掘一个新的区块,方法是在新标签的地址栏中键入localhost:3003/mine。响应将指示区块已成功挖掘和广播:

从前面的屏幕截图中,在transactions数组中,看起来我们创建的所有交易都存在于新挖掘的区块中。让我们去所有的节点,验证我们创建的交易是否已添加到新的区块中。在localhost:3001上,您可以观察到以下输出:

从这个屏幕截图中,我们可以观察到我们现在有了一个包含我们发送的所有交易的第四个区块。然后,如果您检查pendingTransactions数组,您会看到交易数据已被清除,并且新的挖矿奖励存在其中:

在本节中,我们在不同的节点上创建了一对新的交易。然后,这些交易成功地被广播到整个网络。然后,我们挖掘了一个新的区块,我们创建的所有交易都成功地添加到了新的区块中。除此之外,我们新挖掘的区块被广播到了区块链网络中的所有节点。我们整个网络中的所有节点现在都是同步的,并且都包含相同的区块链数据。

摘要

到目前为止,您在本书中取得了很大的成就。您已经创建了一个分散的区块链网络,目前正在五个节点上运行,并且您构建了功能,以同步整个网络,以便所有节点都具有完全相同的数据。这反映了区块链在实际应用中的功能。

在本章中,我们通过重构端点将整个区块链网络成功同步,将数据广播到网络中的所有节点。我们首先将/createNewTransaction方法的功能拆分为两个部分:/createNewTransaction方法和addTransactionToPendingTransactions方法。然后,我们构建了/transaction/broadcast端点,将新创建的交易广播到网络中的所有节点。我们还重构了/transaction端点,使得/transaction/broadcast端点和/transaction端点能够一起工作。在本章的后面,我们重构了/mine端点,并构建了一个新的端点/receive-new-block。借助这些端点,新创建的区块可以广播到网络中的所有节点。

在下一章中,我们将构建共识算法,以确保网络中的所有节点都能就区块链中应持有的正确数据达成一致。

第六章:共识算法

在本章中,我们将为区块链网络构建一个共识算法。共识算法是所有网络内的节点就哪些数据是正确的并应该保留在区块链中达成一致的一种方式。为了构建共识算法,我们首先将构建一个名为chainIsValid的新方法。这个方法将通过比较链中所有区块的所有哈希来简单验证区块链。之后,我们将构建一个/consensus端点,每当我们想使用共识算法时,我们将访问该端点。

在本章中,我们将学习以下内容:

  • 共识算法是什么

  • 构建和测试chainIsValid方法

  • 构建和测试/consesnsus端点

所以,让我们开始共识算法。

共识算法是什么?

当构建区块链时,它正在数百或数千个节点之间运行,并且每个交易和每个被创建的区块都被广播到整个区块链网络。在这些广播过程中可能会出现问题,或者可能某个节点没有收到发生的某个信息或交易。

甚至在区块链网络中可能存在一个恶意行为者,他在他们的区块链副本上发送虚假信息或创建欺诈性交易,并试图将它们广播到整个网络,以说服每个人它们是合法交易。那么,我们如何解决这个问题,以便区块链网络中只有合法的区块?

这就是共识算法将帮助我们的地方。共识算法将为我们提供一种比较一个节点与网络中所有其他节点的方式,以确认我们在该特定节点上有正确的数据。目前有许多不同的共识算法被用于不同的区块链网络。对于我们的区块链网络,我们将创建一个实现最长链规则的共识算法。

基本上,最长链规则会查看单个节点和该节点上的区块链副本,将该节点上的链的长度与所有其他节点上的链的长度进行比较。在这种比较中,如果发现有一条链的长度比所选节点上的链长,算法将用网络中最长的链替换所选节点上的链。

使用这个方法的理论是,我们应该能够相信最长的链来保存正确的数据,因为创建该链的工作量最大。最长的链中包含最多的区块,每个区块都是通过工作证明进行挖掘的。因此,我们可以假设整个网络都为最长的链做出了贡献,因为这条链需要付出很多工作。因此,我们将使用实现最长链规则的共识算法。比特币区块链网络实际上在现实生活中实现了这个最长链规则。

构建 chainIsValid 方法

让我们开始构建共识算法,创建一个名为chainIsValid的新方法。这个方法将验证一条链是否合法。让我们开始构建这个方法:

  1. blockchain.js文件中,在proofOfWork方法之后,让我们定义该方法如下:
Blockchain.prototype.chainIsValid = function() {

}
  1. 现在,这个方法将以blockchain作为参数,并且将返回blockchain是否有效:
Blockchain.prototype.chainIsValid = function(blockchain) {

}

当我们将它们与当前节点上托管的链进行比较时,我们将使用chainIsValid方法来验证网络中的其他链。为了验证区块链的合法性,我们只需遍历区块链中的每个区块,并验证所有哈希是否正确对齐。

你可能还记得第二章中提到的,当定义createNewBlock方法时,该方法包括previousBlockHashhash属性。这个hash属性是当前区块的哈希值。为了构建chainIsValid方法,让我们遍历区块链中的每个区块,并确保给定区块的previousBlockHash属性与上一个区块中的哈希属性完全相同。让我们在方法内部定义这个条件如下:

  1. 为了遍历区块链中的每个区块,我们将使用一个for循环:
Blockchain.prototype.chainIsValid = function(blockchain) {

       for (var i = 1; i < blockchain.length; i++) {

 }; 

};
  1. 在这个for循环内,让我们比较当前区块和上一个区块:
Blockchain.prototype.chainIsValid = function(blockchain) {

       for (var i = 1; i < blockchain.length; i++) {
                const currentBlock = blockchain[i];
 const prevBlock = blockchain[i - 1];   
       };  

};

当我们在每次迭代中遍历整个链时,currentBlock将是i的值,prevBlock将是i - 1的值。

  1. 接下来,我们只需比较currentBlock上的previousBlockHash属性与上一个区块上的哈希属性。为了做到这一点,在方法中定义以下条件:
Blockchain.prototype.chainIsValid = function(blockchain) {

       for (var i = 1; i < blockchain.length; i++) {
                const currentBlock = blockchain[i];
                const prevBlock = blockchain[i - 1];
                if (currentBlock['previousBlockHash'] !== prevBlock['hash']) // chain is not valid...

       };  

};

当涉及到我们提到的前一个条件时,如果它没有得到满足,那么我们知道链条是无效的,因为哈希值没有正确对齐。

  1. 为了满足验证条件,当前区块上的previousBlockHash应该等于上一个区块的哈希。我们将在方法内部使用一个标志来表示上述条件,如下所示:
Blockchain.prototype.chainIsValid = function(blockchain) {
       let validChain = true; 
       for (var i = 1; i < blockchain.length; i++) {
                const currentBlock = blockchain[i];
                const prevBlock = blockchain[i - 1];
                if (currentBlock['previousBlockHash'] !== prevBlock['hash']) // chain is not valid...   
       };  

};

最初,validChain变量的值等于true。当我们遍历区块链并看到哈希值没有正确对齐时,我们会将validChain变量设置为false,以表示链条无效。

  1. 现在让我们回到if语句。将上述条件添加到其中:
Blockchain.prototype.chainIsValid = function(blockchain) {
       let validChain = true; 
       for (var i = 1; i < blockchain.length; i++) {
                const currentBlock = blockchain[i];
                const prevBlock = blockchain[i - 1];
                if (currentBlock['previousBlockHash'] !== prevBlock['hash']) validChain = false;   
       };  

};
  1. 在循环结束时,我们可以简单地返回一个validChain变量,如果链有效,则返回值为true,如果无效则返回false
Blockchain.prototype.chainIsValid = function(blockchain) {
       let validChain = true; 
       for (var i = 1; i < blockchain.length; i++) {
                const currentBlock = blockchain[i];
                const prevBlock = blockchain[i - 1];
                if (currentBlock['previousBlockHash'] !==
                prevBlock['hash']) validChain = false;   
       };  
       return validChain;
};
  1. 我们还要做的一件事是验证链中的每个区块是否都具有正确的数据。我们可以通过使用hashBlock方法重新计算currentBlock的哈希值来实现这一点。如果生成的哈希值以四个零开头,就像我们在第二章中看到的那样,那么我们知道所有数据都是有效的。然而,如果不是以四个零开头,那么我们知道区块内的数据肯定是无效的。

我们要做的就是遍历链中的每个区块,重新计算每个区块的哈希值,并确保每个哈希值以四个零开头。因此,在for循环内,让我们首先定义一个变量来提到这个条件:

Blockchain.prototype.chainIsValid = function(blockchain) {
       let validChain = true; 
       for (var i = 1; i < blockchain.length; i++) {
                const currentBlock = blockchain[i];
                const prevBlock = blockchain[i - 1];
                const blockHash = this.hashBlock ();
                if (currentBlock['previousBlockHash'] !==
                prevBlock['hash']) validChain = false;   
       };  
     return validChain;
};
  1. hashblock()方法接受参数,如:previousBlockhashcurrentBlockDatanonce。让我们现在传递这些参数:
const blockHash = this.hashBlock (prevBlock['hash']);
  1. 接下来,我们必须将currentBlockData作为参数传递,你可能还记得前一章中提到的,它包括currentBlock中的交易和currentBlock的索引:
const blockHash = this.hashBlock(prevBlock['hash'], { transactions: currentBlock['transactions'], index: currentBlock['index'] } );
  1. 最后,我们必须传递的最后一个参数是nonce
const blockHash = this.hashBlock (prevBlock['hash'], { transactions: currentBlock['transactions'], index: currentBlock['index'] } currentBlock['nonce']);
  1. 定义这些参数后,我们应该将currentBlock的哈希存储在blockHash变量中。接下来,我们只需验证哈希是否以四个零开头。因此,在for循环内,我们将提到以下条件:
if (blockHash.substring(0, 4) !== '0000') validChain = false;

现在,我们基本上是在遍历整个区块链,只是简单地检查两件事:

  • 我们进行的一个检查是确保所有哈希值正确对齐。如果它们没有正确对齐,我们会指出链条无效。

  • 我们正在进行的另一个检查是对每个区块进行哈希,并确保blockHash字符串以四个零开头。如果不是以四个零开头,那么我们指出链条无效。

现在chainIsValid方法基本上已经完成了。然而,您可能已经注意到的一个重要的事情是,我们还没有检查创世区块是否符合任何方法。在我们在前面的代码块中定义的循环中,我们从位置 1 开始,完全跳过了位置 0,即创世区块。创世区块是一种特殊的区块,因为我们自己制作了它,而没有进行工作证明:

  1. 因此,为了验证创世区块,我们只需确保它具有我们最初放入其中的属性。因此,在for循环之外,我们将如下表述这个条件:
const genesisBlock = blockchain[0];
  1. 现在我们只是想检查并验证创世区块上的所有属性是否正确。如果您还记得在第二章中,我们定义了创世区块,我们为其分配了值,例如nonce,值为100previousBlockHash,值为0,以及字符串 0 的hash。因此,现在让我们检查这些属性,以确保它们是正确的。在以下代码片段中,我们将上述代码添加到以下变量中:
const genesisBlock = blockchain[0];
const correctNonce = genesisBlock['nonce'] === 100;
const correctPreviousBlockHash = genesisBlock['previousBlockHash'] === '0';
const correctHash = genesisBlock['hash'] === '0';
  1. 最后,我们要验证创世区块中不应该有任何交易。因此,为了检查这一点,我们将提到以下条件:
const correctTransactions = genesisBlock['transactions'].length === 0;
  1. 现在,如果我们有一个合法的创世区块,那么我们定义的所有这些变量都应该是 true。如果任何这些变量无效,那么我们希望将validChain变量更改为false,以便我们知道区块链无效。让我们将这个条件表述如下:
if (!correctNonce || !correctPreviousBlockHash || !correctHash || !correctTransactions) validChain = false;

提及这最后一个条件完成了chainIsValid方法。

测试chainIsValid方法

现在让我们通过实施以下步骤来测试chainIsValid方法:

  1. test.js文件中,让我们导入区块链数据结构并创建一个名为bitcoin的区块链的新实例:
const Blockchain = require('./blockchain');
const bitcoin = new Blockchain();
  1. 接下来,让我们生成一个用于测试的区块链。我们将通过从其中一个服务器开始来实现这一点。因此,转到终端,输入npn run node_1并按Enter。然后您将收到响应,监听端口 3001。

  2. 在节点3001上,现在让我们创建一个区块链并向其中添加一些数据,以便我们可以测试新的区块链。目前,节点3001上的区块链只有创世区块。因此,通过命中/mine端点,让我们向链中添加几个更多的区块。因此,在浏览器中,转到localhost:3001/mine以创建一个新的区块。

  3. 现在,如果您转到localhost:3001/blockchain,您应该能够观察到新的区块如下:

因此,在节点3001,我们现在有两个区块和一个待处理的交易,即挖矿奖励交易。

  1. 接下来,让我们创建一些要添加到区块链中的交易。要添加交易,请转到 Postman,并在那里添加一些交易,如下截图所示。让我们将这些交易发送到localhost:3001,并且还要命中/transaction/broadcast端点:

  1. 您也可以向节点添加许多其他交易。

  2. 一旦交易被添加,让我们通过访问localhost:3001/mine来挖掘一个新的区块。一旦新的区块被挖掘出来,访问localhost:3001/blockchain以验证该区块是否已被添加到网络中。您应该观察到以下输出:

您将看到节点3001包含了第三个区块,其中包含我们在区块中传递的所有交易数据。我们还有一个待处理的交易。

  1. 接下来,让我们向节点3001添加几个更多的交易,然后在该节点上挖掘一个新的区块。您将看到与前面情况类似的输出。我们添加的新交易数据现在存在于我们挖掘的第四个区块中。请查看以下截图:

  1. 接下来,让我们再挖掘两个没有任何数据的块。现在,我们有一个包含六个块的区块链。在这六个块中,有两个块中没有任何交易数据。

  2. 复制localhost:3001上的整个区块链并将其粘贴到test.js文件中。然后,在test.js文件中粘贴数据后,让我们将该粘贴的文本保存为一个变量:

const bc1 { //.... the entier blockchain that we copied and pasted };
  1. 让我们使用chainIsValid方法来验证链的有效性。为了做到这一点,在test.js文件中,让我们提到以下内容:
console.log('VALID:' , bitcoin.chainIsValid(bc1.chain));
  1. 让我们保存test.js文件并运行它。

验证测试的输出

现在,当我们运行这个文件时,我们应该收到一个有效区块链的验证,因为我们没有篡改它,而是合法地使用了所有正确的方法创建它。让我们验证chainIsValid方法是否正常工作:

  1. 前往终端并通过在终端中键入^C来取消之前正在运行的进程。

  2. 一旦进程被取消,然后在终端中,让我们键入node dev/test.js并按Enter。由于我们没有篡改区块链,我们将得到Valid: true的反馈,如下面的截图所示:

现在,让我们稍微篡改一下区块链,看看是否可以得到一个错误的返回值:

  1. 在我们粘贴到test.js文件中的区块链数据中,让我们更改任一块中的一个哈希值,看看是否会使区块链无效。

  2. 一旦你改变了任何块的哈希值,保存文件并再次运行测试。由于数据现在被篡改,你将得到false的反馈:

接下来,让我们在一个区块的交易数据中搞一些乱。如果我们更改了一个区块中的任何交易数据,那么链就不应该是有效的,我们应该收到测试的假反馈。

最后,让我们测试创世块,也就是链中的第一个块:

在我们粘贴的区块链数据的test.js文件中,让我们将nonce值从 100 改为 10。保存文件并在终端中再次运行测试,我们应该得到返回的输出为false。由于我们在test.js文件中篡改了区块链中的数据,当我们运行测试时,我们得到了false的反馈。这表明区块链不再有效或合法,因为其中的数据已经被篡改。因此,从这个测试中我们可以得出结论,chainIsValid方法完全符合我们的预期。

对结果进行适当的微小修改

现在,我们需要做的一个小事情是帮助我们理解chainIsValid方法的工作原理,即记录每个块的previousBlockHashcurrentBlock哈希值,以便我们自己进行比较。因此,在chainIsValid方法的for循环中,让我们在循环结束之前添加以下代码行:

console.log('previousBlockHash =>', prevBlock [ 'hash']);
console.log('currentBlockHash =>', currentBlock [ 'hash']);

让我们保存这个修改并再次运行测试。这一次,当我们运行测试时,我们应该看到所有的哈希值被记录下来,这样我们就可以自己比较它们,看看这个方法内部到底发生了什么。运行测试后,你应该看到previousBlockHashcurrentBlockHash的值,如下面的截图所示:

从前面的截图中,你可以观察到,对于每次迭代,previousBlockHash的值都与前一个块的currentBlockHash的值匹配。如果你看所有的哈希值,你会看到它们成对地被记录下来。从截图中,我们可以观察到我们有许多对相同的哈希值,这就是使区块链有效的原因。

构建/共识端点

现在,让我们构建/consensus端点,它将使用我们在上一节中构建的chainIsValid方法。执行以下步骤来构建端点:

  1. 让我们转到networkNode.js文件,并在/register-node-bulk端点之后,定义/consensus端点如下:
app.get('/consensus', function(req, res) { 

});
  1. 接下来,在/consensus端点内,让我们向区块链网络中的每个其他节点发出请求,以获取它们的区块链副本,并将其与当前节点上托管的区块链副本进行比较:
app.get('/consensus', function(req, res) {
        bitcoin.networkNodes.forEach(networkNodeUrl => {

 }); 

});
  1. 在这个forEach循环内,让我们做与在前几章中定义其他端点时做过无数次的相同的事情。因此,我们首先要为请求定义一些选项,如下所示:
app.get('/consensus', function(req, res) {
        bitcoin.networkNodes.forEach(networkNodeUrl => {
                const requestOptions = {
 uri: networkNodeUrl + '/blockchain',
 method: 'GET',
 json: true 
 }        

        });         

});
  1. 在定义选项之后,我们需要request-promise requestOptions,并将所有这些请求推入一个承诺数组,因为每个请求都会向我们返回一个承诺:
app.get('/consensus', function(req, res) {
        const requestPromises = [];
        bitcoin.networkNodes.forEach(networkNodeUrl => {
                const requestOptions = {
                        uri: networkNodeUrl + '/blockchain',
                        method: 'GET',
                        json: true 
                }        
                requestPromises.push(rp(requestOptions));
        });         

});
  1. 一旦forEach循环运行后,我们将得到一个填满所有请求的数组。接下来,让我们按以下方式运行这些请求:
app.get('/consensus', function(req, res) {
        const requestPromises = [];
        bitcoin.networkNodes.forEach(networkNodeUrl => {
                const requestOptions = {
                        uri: networkNodeUrl + '/blockchain',
                        method: 'GET',
                        json: true 
                }        
                requestPromises.push(rp(requestOptions));
        });         
        Promise.all(requestPromises) 
  1. 然后,让我们使用从所有这些承诺中收到的数据。我们收到的这些数据将是来自网络中每个节点的区块链的数组。因此,在上述代码的后面,让我们定义如下的代码:
.then(blockchains => {

});
  1. 现在让我们遍历来自网络中其他节点的所有这些blockchains,并查看是否有一个比当前节点上托管的区块链副本更长的区块链。我们将从响应中获取的所有区块链中开始循环:
.then(blockchains => {
        blockchains.forEach(blockchain => { 
 //....
 });
});
  1. 基本上,在forEach循环内,我们要做的就是确定网络中其他节点的区块链是否比当前节点上托管的区块链更长。为了做到这一点,让我们定义一些变量来跟踪所有数据,如下所示。我们要定义的第一个变量是托管在当前节点上的区块链的长度:
.then(blockchains => {
        const currentChainLength = bitcoin.chain.length;
        blockchains.forEach(blockchain => {                
            //....
        });
});
  1. 接下来,让我们定义一个变量,如果在blockchains数组中遇到更长的区块链,它将发生变化。我们要定义的第一件事是maxChainLength变量:
.then(blockchains => {
        const currentChainLength = bitcoin.chain.length;
        let maxChainLength = currentChainLength;
        blockchains.forEach(blockchain => {                
            //....
        });
});
  1. 接下来,我们要定义一个名为newLongestChain的变量。最初,我们将把它设置为null
.then(blockchains => {
        const currentChainLength = bitcoin.chain.length;
        let maxChainLength = currentChainLength;
        let newLongestChain = null;
        blockchains.forEach(blockchain => {                
            //....
        });
});
  1. 然后,我们要定义的最后一个变量将被称为newPendingTransactions。让我们最初将其设置为null
.then(blockchains => {
        const currentChainLength = bitcoin.chain.length;
        let maxChainLength = currentChainLength;
       let newLongestChain = null;
        let newPendingTransactions = null;
        blockchains.forEach(blockchain => {                
            //....
        });
});
  1. 现在,在forEach循环内,我们要查看区块链网络中是否存在比当前节点上更长的链。如果网络中存在更长的链,那么改变上述变量以反映这一点。因此,在forEach循环内,定义如下的this条件:
.then(blockchains => {
        const currentChainLength = bitcoin.chain.length;
        let maxChainLength = currentChainLength;
       let newLongestChain = null;
        let newPendingTransactions = null;
        blockchains.forEach(blockchain => {                
            if (blockchain.chain.length > maxChainLength) {
 maxChainLength = blockchain.chain.length;
 newLongestChain = blockchain.chain;
 newPendingTransactions =
 blockchain.pendingTransactions;
 };    
        });
});

现在,在forEach循环运行后,我们将拥有确定是否需要替换托管在当前节点上的链所需的所有数据。接下来,在循环之后,让我们定义以下条件:

if (!newLongestChain || (newLongestChain &&
    !bitcoin.chainIsValid(newLongestChain))) 
{
         res.json({
             note: 'Current chain has not been replaced.',
             chain: bitcoin.chain
         });
}

基本上,在这个if语句中我们要表达的是,如果没有newLongestChain,那么当前链就是最长的。或者,如果有一个新的最长链,但是这个新链无效,那么在这两种情况下,我们都不想替换托管在当前节点上的区块链。因此,我们将发送回一个说明“当前链未被替换”的通知。

否则,如果有一个newLongestChain并且该链是有效的,那么现在我们要用网络中最长的链替换托管在当前节点上的区块链。我们将在 else 块中定义所有这些内容,如下所示:

else {
         bitcoin.chain = newLongestChain;
         bitcoin.pendingTransactions = newPendingTransactions;
         res.json({
                       note: 'This chain has been replaced.',
                       chain: bitcoin.chain
         });
}

构建过程的快速回顾

在这个端点中,我们首先向网络中的所有其他节点发出请求,以便我们可以访问每个节点上托管的区块链。在我们运行了所有这些请求之后,我们就可以访问网络中所有其他节点上托管的所有区块链。然后,我们通过forEach循环遍历网络中所有其他区块链。当我们遍历其他区块链时,如果我们找到了更长的链,我们就会更新maxChainLengthnewLongestChainnewPendingTransactions变量以反映出这一点。然后,当forEach循环完成时,我们就会知道网络中是否存在比当前节点上托管的区块链更长的链。如果在网络中找到了更长的链,我们将能够访问该区块链的pendingTransactions。因此,在forEach循环运行后,我们将能够访问所有必要的数据,以替换当前节点上托管的错误区块链。

然后,我们说明了是否存在新的更长链,或者是否存在比当前节点上托管的区块链更长的链。如果在网络中存在更长的链,但该链无效,那么在这两种情况下,我们都不希望替换当前节点上托管的区块链,因此我们只需发送一个响应,说明当前链未被替换。

另一方面,如果在网络中存在更长的链,并且该链是有效的,那么我们将希望替换当前节点上托管的区块链。我们只需发送一个响应,说明该链已被替换,并返回新的区块链。

这就是共识算法和/consensus 端点的工作原理。

测试/consensus 端点

让我们测试刚刚构建的共识端点。因此,这个/consensus 端点应该做什么?当我们在特定节点上调用/consensus 端点时,它应该为我们确认该特定节点是否具有正确的区块链数据,并且该节点与网络的其余部分是同步的。让我们开始构建测试:

  1. 我们的第一步是建立一个由前四个节点组成的网络。因此,让我们去 Postman,并在托管在3001上的节点上点击 register-and-broadcast-node 端点。

  2. 让我们像下面的屏幕截图中所示,将第二个节点添加到网络中。然后,我们将点击发送按钮,接收到响应,成功注册新节点到网络:

  1. 同样地,您可以将剩余的节点30033004注册到网络中。现在,如果您去浏览器并检查所有节点,您将观察到从30013004的所有节点都相互连接,但节点 3005 没有连接。

  2. 接下来,我们想要在区块链网络上挖掘一些区块,除了第五个节点。因此在浏览器中,让我们访问localhost:3001/mine。这将在节点3001上为我们挖掘一个区块。

  3. 同样地,让我们在localhost:3003上挖掘两个区块,在localhost:3004上挖掘一个区块。现在,所有这些节点应该都有五个区块。您可以通过在浏览器中输入localhost:3001/blockchain来验证这一点。您将能够观察到我们刚刚添加的所有五个区块。

  4. 在这一点上,我们想要将第五个节点连接到区块链网络。因此,让我们去 Postman 并发送 3005 的请求,如下面的屏幕截图所示:

  1. 现在,节点3005应该已连接到网络。您可以通过浏览器验证这一点:

现在3005是网络的一部分,问题就出现在这里:节点3005在区块链中没有正确的区块数据。它应该拥有其他节点拥有的所有五个区块。这就是/consensus端点发挥作用的地方。我们应该能够访问/consensus端点并解决这个问题。在这之后,我们应该期望节点3005上的区块链与网络中的其他所有节点具有相同的数据。

现在让我们试一试。在浏览器中打开另一个标签,并在地址栏中输入localhost:3005/consensus,然后按下Enter运行它。您应该观察到类似于以下截图中所见的输出:

在前面的截图中,我们得到了响应,链已被替换,然后新的区块链数据取代了这个节点上的旧数据。让我们通过在浏览器中打开另一个标签并访问localhost:3005/blockchain来验证这个节点。您会看到网络中存在的所有区块都已经添加到节点3005中。因此,节点3005现在拥有了正确的区块链数据。我们通过访问节点3005上的/consensus端点来实现了这一点。现在,区块链网络中的所有节点应该具有完全相同的数据。

现在,如果你再次尝试在3005节点上访问/consensus端点,我们将会得到以下响应:

我们收到这样的响应,是因为在之前运行共识端点时,网络中已经存在的所有区块都已经添加到节点3005中。

通过这个测试,我们可以得出结论,/consensus完美地按预期工作。/consensus端点有能力在区块链中纠正节点的错误数据。

建议您尝试使用/consensus端点以不同的方式进行测试。向数据添加一些交易,并确保它能够正确解决持有错误数据的节点。通过更多地测试这个端点,您将更加熟悉它在底层是如何工作的。

摘要

所有的区块链都有共识算法,在本章中,我们构建了自己的共识算法,实现了最长链规则。我们首先构建了chainIsValid方法。在这个方法中,我们简单地遍历了区块链中的每一个区块,并比较了每个区块上的哈希值,以确保它们是正确的。然后我们继续测试这个方法。除此之外,我们利用chainIsValid方法构建了/consensus端点。

在下一章中,我们将构建一个区块浏览器,我们将能够在浏览器上访问。这个区块浏览器将允许我们通过用户界面与区块链进行交互。

第七章:区块浏览器

在这一章中,让我们构建一个区块浏览器,它将允许我们与区块链进行交互。区块浏览器只是一个用户界面,它将允许我们探索区块链内部的数据。它将允许我们搜索特定的区块、特定的交易或特定的地址,然后以视觉上吸引人的格式显示特定的信息。

构建区块浏览器的第一步是向区块链添加一些新的方法和端点,以便搜索数据。然后,让我们为区块浏览器添加一个前端,以便我们可以在浏览器中使用它。

在本章中,我们将涵盖以下主题:

  • 什么是区块浏览器?

  • 定义区块浏览器端点

  • 构建getBlockgetTransactiongetAddressData方法

  • 构建和测试/block/:blockHash/transaction/:transactionId/address/:address端点

  • 开发我们的区块浏览器界面并对其进行测试。

因此,让我们开始构建我们的区块浏览器。

什么是区块浏览器?

区块浏览器是一个在线平台,允许您浏览区块链,搜索包括地址、区块、交易等各种内容。例如,如果您访问www.blockchain.com/explorer,您可以看到比特币和以太坊区块链的区块浏览器实用程序,如下所示:

在这个区块浏览器内,您可以搜索整个区块链以获取特定的区块、哈希或交易,或者任何其他所需的数据片段。该实用程序还在易于理解的界面上显示结果。例如,如果我们在区块浏览器中搜索Block #549897,您将看到该特定区块的所有细节,如下截图所示:

这正是我们将在本章中为我们的区块链构建的内容。

定义区块浏览器端点

为了使区块浏览器正常运行,我们需要查询区块链以获取地址、区块哈希和交易 ID,以便我们可以搜索特定的参数并得到相应的数据。因此,我们需要执行的第一步是构建一些新的端点。为此,让我们继续以下步骤:

  1. 转到dev/networkNode.js文件,在/consensus端点之后,让我们定义我们的区块浏览器的第一个端点/block/:blockHash,如下所示:
app.get('/block/:blockHash', function(req, res) { 

});

通过这个端点发送一个特定的blockHash,结果将简单地返回与输入的blockHash对应的区块。

  1. 我们将构建的下一个端点将是/transaction/:transactionId。定义如下:
app.get('/transaction/:transactionId', function(req, res) {

});

通过这个端点发送一个transactionId,作为回应,我们应该期望得到与该 ID 对应的正确交易。

  1. 最后,我们将构建的第三个端点是/address/:address,定义如下:
app.get('/address/:address', function(req, res) {

});

通过这个端点,我们将发送一个特定的地址,作为回应,您应该期望得到与该地址对应的所有交易——每当这个特定地址发送或接收比特币时——您还将了解到该地址的当前余额,即该地址当前拥有多少比特币。

因此,在本章中,您将构建这三个端点。对于这些端点中的每一个,我们将在区块链数据结构中构建一个特定的方法,该方法将查询区块链以获取正确的数据片段。因此,让我们创建查询区块链特定区块哈希、交易和地址的方法。

构建 getBlock 方法

让我们构建一个名为getBlock的新方法,该方法将获取给定的blockHash并搜索整个区块链,以找到与该特定哈希相关联的区块。为了构建getBlock方法,请按照以下步骤进行:

  1. 转到dev/blockchain.js文件,在chainIsValid方法之后,定义如下新方法:
Blockchain.prototype.getBlock = function(blockHash) { 

};
  1. 在这个方法中,我们要遍历整个区块链,搜索具有特定blockHash值的区块。然后,该方法将把该特定区块返回给我们。我们将借助for循环来完成所有这些操作:
Blockchain.prototype.getBlock = function(blockHash) { 
    this.chain.forEach(block => {

 });
};

在定义for循环时,我们遍历区块链中的每个区块。

  1. 接下来,在循环内,使用if语句来说明条件,如下所示:
Blockchain.prototype.getBlock = function(blockHash) { 
    this.chain.forEach(block => {
            if (block.hash === blockHash) 
    });
};
  1. 为了表示我们正在寻找的正确区块已找到,我们将使用一个标志。让我们按照以下代码中的突出显示定义此标志变量:
Blockchain.prototype.getBlock = function(blockHash) { 
    let correctBlock = null;
    this.chain.forEach(block => {
            if (block.hash === blockHash) 
    });
};
  1. 当我们遍历链中的所有区块时,如果找到正确的区块,我们将把它赋给correctBlock。让我们按照以下条件来说明:
Blockchain.prototype.getBlock = function(blockHash) { 
  let correctBlock = null;
    this.chain.forEach(block => {
            if (block.hash === blockHash) correctBlock = block;  
    });
};
  1. 最后,在此方法的末尾,我们要返回correctBlock,如下所示:
Blockchain.prototype.getBlock = function(blockHash) { 
  let correctBlock = null;
    this.chain.forEach(block => {
            if (block.hash === blockHash) correctBlock = block;  
    });
    return correctBlock
};

构建/block/:blockHash端点

/block/:blockHash端点内使用getBlock方法来通过blockHash检索特定区块。让我们按照以下步骤构建端点:

  1. 在此端点中,我们要做的第一件事是使用发送到/block/:blockHash请求的blockHash值。我们可以在req.params对象上访问此blockHash。转到dev/networkNode.js文件,并在先前定义的/block/:blockHash端点中添加以下突出显示的代码:
app.get('/block/:blockHash', function(req, res) { 
        const blockHash = req.params.blockHash;
});

基本上,当我们访问/block/:blockHash端点时,我们正在访问网络中特定节点上存在的区块的哈希值。我们还将使用req.params对象来访问哈希值,这将使我们能够访问/block/:blockHash URL 中带有冒号的任何值。因此,当用户向此端点发出请求时,他们将在 URL 中发送一个blockHash,然后我们可以借助req.params.blockHash来获取该blockHash。然后,我们将保存该值在blockHash变量中。

  1. 接下来,在端点内,我们要使用在上一节中创建的getBlock方法。我们将在端点中添加该方法,如下面的代码所示:
app.get('/block/:blockHash', function(req, res) { 
        const blockHash = req.params.blockHash; const correctBlock = bitcoin.getBlock(blockHash);
});

到了代码的这一点,我们正在寻找的区块应该存在于correctBlock变量中。

  1. 最后,将correctBlock变量作为响应发送回去,因此让我们在端点中添加以下突出显示的代码:
app.get('/block/:blockHash', function(req, res) { 
        const blockHash = req.params.blockHash;const correctBlock = bitcoin.getBlock(blockHash);
        res.json({
 block: correctBlock
 });
});

这就是我们使用getBlock方法构建/block/:blockHash端点的方式。现在,让我们测试此端点并验证其是否正常工作。

测试/block/:blockHash端点

为了测试/block/:blockHash端点,请按照以下步骤进行:

  1. 首先检查区块链中有多少个区块。转到浏览器,输入localhost:3001/blockchain,然后按Enter。您将看到区块链中存在的单个创世区块,如下所示:

  1. 您需要向此链中添加几个区块。要做到这一点,转到浏览器中的另一个标签页,输入localhost:3001/mine,然后按Enter。使用相同的过程,让我们生成一个更多的区块。现在我们应该在链中有三个区块:一个创世区块和我们刚刚添加的两个区块。

  2. 为了测试/block/:blockHash端点,让我们简单地取其中一个区块的哈希值并用它来测试端点。让我们复制链中第三个区块的哈希值,如下截图所示:

  1. 接下来,转到浏览器中的另一个标签页。在地址栏中键入localhost:3001/block,然后粘贴我们直接复制的哈希值。查看以下截图以更好地理解:

  1. 现在,我们知道我们使用的哈希存在于链中的第三个区块中。因此,我们应该期望通过运行/block/:blockHash端点来返回第三个区块。现在按Enter,正确的区块应该作为输出返回给我们:

从上面的截图中,我们可以观察到正确的区块已经返回给我们。返回的区块包括我们在/block/:blockHash端点中使用的哈希值来搜索区块。

以类似的方式,您现在可以尝试使用端点和特定区块的哈希值来搜索链中的另一个区块。

现在,如果我们发送错误的哈希或在端点中不存在的哈希,我们应该期望得到 null 作为输出,而不是返回区块。让我们尝试通过向/block/:blockHash端点发送错误的哈希值来验证这一点。在浏览器的地址栏中,键入localhost:3001/block,然后添加一个虚假的哈希值并按Enter。应返回以下输出:

从上面的截图中,您可以观察到block等于null。这意味着用于搜索区块的哈希值在链中不存在。因此,从测试中,我们可以得出结论,/block/:blockHash端点完全按预期工作。

定义 getTransaction 方法

让我们在区块链数据结构上添加一个名为getTransaction的新方法。这将允许我们通过传递transactionId来获取特定交易。我们将在/transaction/:transactionId端点内使用这个新方法。所以,让我们开始吧!

  1. 转到dev/blockchain.js文件,在getBlock方法之后,定义getTransaction如下:
Blockchain.prototype.getTransaction = function(transactionId) { 

}):

这个方法与getBlock方法非常相似。在这里,我们将遍历整个链,并将一个标志设置为我们正在寻找的正确交易。

  1. 构建此方法的下一步是遍历整个区块链。为此,使用forEach循环如下所示:
Blockchain.prototype.getTransaction = function(transactionId) { 
       this.chain.forEach(block => { 

 });

}):
  1. 由于在这个方法中,我们正在寻找交易,我们需要遍历链中每个区块上的每个交易。因此,我们需要在前面的for循环内添加另一个for循环:
Blockchain.prototype.getTransaction = function(transactionId) { 
       this.chain.forEach(block => { 
               block.transactions.forEach(transaction => { 

 });
       });

});
  1. 现在,我们可以访问区块链上的每个交易,我们只需要将每个交易的transactionId与我们正在寻找的transactionId进行比较。当两者匹配时,我们就知道找到了正确的交易。让我们在循环内定义这个条件如下:
Blockchain.prototype.getTransaction = function(transactionId) { 
       this.chain.forEach(block => { 
               block.transactions.forEach(transaction => { 
                       if (transaction.transactionId === transactionId) {

 }; 
               });
       });

});
  1. 接下来,就像我们在getBlock方法内部所做的那样,我们希望在getTransaction方法内部设置一个标志,以指示我们已经找到了正确的交易。因此,在两个循环的顶部,定义标志变量并如下使用它:
Blockchain.prototype.getTransaction = function(transactionId) {
       let correctTransaction = null; 
       this.chain.forEach(block => { 
               block.transactions.forEach(transaction => { 
                       if (transaction.transactionId === transactionId) {
                               correctTransaction = transaction;         

                       }; 
               });
       });

});
  1. 现在,为了使这个方法更有用一些,我们还将发送回我们找到所需交易的区块。为此,定义另一个标志如下:
let correctBlock = null;
  1. 如果我们找到了正在寻找的交易,将条件设置如下:
Blockchain.prototype.getTransaction = function(transactionId) {
       let correctTransaction = null;
       let correctBlock = null;  
       this.chain.forEach(block => { 
               block.transactions.forEach(transaction => { 
                       if (transaction.transactionId === transactionId) {
                             correctTransaction = transaction;         
                               correctBlock = block; 
                       }; 
               });
       });

});
  1. 最后,要做的最后一件事就是将两个变量作为输出返回。让我们在两个循环之外定义这个返回条件如下:
return {
         transaction: correctTransaction,
         block: correctBlock
};

构建/transaction/:transactionId 端点

让我们使用在上一节中构建的getTransaction方法来构建/transaction/:transactionId端点。让我们开始吧:

  1. 在这个端点内部要做的第一件事是存储作为请求参数发送的交易 ID。让我们将其存储在一个transactionId变量中,如下所示:
app.get('/transaction/:transactionId', function(req, res) {
         const transactionId = req.params.transactionId;
});
  1. 接下来要做的是在端点内部使用getTransaction方法。为此,请将以下内容添加到前面的代码中:
app.get('/transaction/:transactionId', function(req, res) {
         const transactionId = req.params.transactionId;
         bitcoin.getTransaction(transactionId);   

});
  1. getTransaction方法中,我们得到一个包含我们正在寻找的交易和该交易所在的区块的对象。我们希望将这些数据存储在一个名为transactionData的变量中,如下所示:
app.get('/transaction/:transactionId', function(req, res) {
         const transactionId = req.params.transactionId;
         const trasactionData = bitcoin.getTransaction(transactionId);  

});
  1. 最后,我们希望发送一个简单的响应,其中包含transactionData变量:
app.get('/transaction/:transactionId', function(req, res) {
         const transactionId = req.params.transactionId;
         const trasactionData = bitcoin.getTransaction(transactionId);
         res.json({
    transaction: trasactionData.transaction,
    block: trasactionData.block
         });   

});

这就是我们构建/transaction/:transactionId端点的方式。

测试/transaction/:transactionId端点

现在,是时候测试/transaction/:transactionId端点,以验证它是否按预期工作。但在这之前,我们需要向区块链添加一些交易数据和区块。

向区块链添加新的交易和区块

与前一部分类似,首先让我们向区块链添加一些交易和区块:

  1. 因此,转到 Postman,点击localhost:3001/transaction/broadcast端点,将交易发送到网络中的所有节点。

  2. 现在,向网络发送一些示例交易。您可以按照以下截图中所示的方式创建交易:

  1. 添加交易数据后,单击发送按钮将交易发送到网络。同样,您可以添加另一笔"amount": 200的交易并将其发送到网络。

  2. 接下来,挖掘一个新的区块,以便将这些交易添加到区块链中。在浏览器中打开一个标签,输入localhost:3001/mine到地址栏。然后将创建新的区块:

  1. 接下来,发送另一个“amount”: 300 的交易,并使用先前提到的过程将其发送到网络。一旦交易发送完毕,让我们再次挖掘一个区块,将交易添加到区块链中:

  1. 现在,添加另外两笔交易,分别为"amount": 400500,并将其发送到网络。最后,再次挖掘一个区块,将我们现在创建的交易添加到区块链中:

现在,如果您转到localhost:3001/blockchain,您将看到我们刚刚添加到区块链中的所有区块和交易。

测试端点

在向区块链添加交易和区块后,让我们测试/transaction/:transactionId端点:

  1. 转到浏览器,打开另一个标签。在地址栏中输入localhost:3001/transaction/,然后在 URL 的末尾添加一个来自区块链中任何一个区块的transactionId值,然后按 Enter。参考以下截图:

  1. 运行此端点后,应返回以下输出:

在前面的截图中,您可以看到我们使用端点传递的transactionId关联的交易作为输出。我们还返回了包含我们正在寻找的特定transactionId的区块。

  1. 现在,使用一个在区块链中不存在的transactionId进行另一个示例。为此,转到浏览器,输入localhost:3001/transaction/到地址栏。在这之后,向端点添加一个随机的哈希值。参考以下截图:

  1. 运行此端点时,您将得到值为 null 的输出,如下截图所示:

在前面的截图中返回的空值告诉我们,这个transactionId在区块链中不存在。

从测试中,我们可以得出结论,/transaction/:transactionId端点和getTransaction方法都正常工作。

构建getAddressData方法

我们将在区块链原型上构建一个名为getAddressData的新方法,并在/address/:address端点内部使用这个方法,以获取我们正在搜索的特定地址的数据:

  1. 让我们在blockchain.js文件中构建这个新方法。在getTransaction方法之后,定义getAddressData方法如下:
Blockchain.prototype.getAddressData = function(address) {

});
  1. 现在,在这个方法内部,我们要做的第一件事是获取与该地址相关的所有交易,并将它们放入一个单一的数组中。让我们现在定义这个数组:
Blockchain.prototype.getAddressData = function(address) {
       const addressTransactions = [];
});
  1. 然后,我们要循环遍历区块链中的所有交易。如果任何这些区块中的交易的接收者或发送者是我们正在搜索的地址,那么我们要将所有这些交易添加到addressTransactions数组中。让我们定义这个条件如下。第一步是循环遍历区块链上的所有区块:
Blockchain.prototype.getAddressData = function(address) {
       const addressTransactions = [];
       this.chain.forEach(block => {

 }); 
});
  1. 现在,为了访问区块链中的交易,我们需要循环遍历每个区块上存在的所有交易。因此,在forEach循环内部,我们将不得不定义另一个forEach循环,如下所示:
Blockchain.prototype.getAddressData = function(address) {
       const addressTransactions = [];
       this.chain.forEach(block => {
               block.transactions.forEach(transaction => {

 });
       }); 
});
  1. 现在,在我们刚刚定义的forEach循环内部,我们可以访问区块链上的每一笔交易。我们只是想测试每笔交易,看看发送者或接收者地址是否与我们正在搜索的地址匹配:
Blockchain.prototype.getAddressData = function(address) {
       const addressTransactions = [];
       this.chain.forEach(block => {
              block.transactions.forEach(transaction => {
                       if(transaction.sender === address ||
 transaction.recipient === address) {
 addressTransactions.push(transaction);
 }
               });
       }); 
});

在代码的这一点上,我们正在循环遍历我们区块链中的所有交易。如果我们遇到一个发送者地址或接收者地址等于我们正在寻找的地址的交易,那么我们将该交易推送到addressTransactions数组中。因此,在两个forEach循环都完成后,我们将得到一个包含与我们正在搜索的地址相关的所有交易的数组。

了解余额

接下来,我们要做的是循环遍历addressTransactions数组,以确定我们正在搜索的地址的余额。为了知道余额:

  1. 让我们首先定义一个名为balance的变量:
let balance = 0;
  1. 接下来,我们要循环遍历addressTransactions数组中的所有交易。我们将使用forEach循环来做到这一点,如下所示:
let balance = 0;
addressTransactions.forEach(transaction => { 

});
  1. 在循环中,使用ifelse-if语句提到条件,如下所示:
let balance = 0;
addressTransactions.forEach(transaction => { 
       if (transaction.recipient === address) balance += transaction.amount;
        else if (transaction.sender === address) balance -= transaction.amount; 
}); 
  1. 最后,在forEach循环结束时,我们要返回一个具有addressTransactions属性的对象,该属性与我们的addressTransactions数组匹配,并且addressBalance也是如此:
let balance = 0;
addressTransactions.forEach(transaction => { 
       if (transaction.recipient === address) balance += transaction.amount;
        else if (transaction.sender === address) balance -= transaction.amount; 
}); 
return {
 addressTransactions: addressTransactions,
 addressBalance: balance
};

有了这个,我们就完成了getAddressData方法的构建。

开发/address/:address 端点

现在,让我们构建/address/:address端点,并在此端点内部使用getAddressData方法。/address/:address端点将与/block/:blockHash/transaction/:transactionId端点非常相似,因此你不应该觉得太具有挑战性:

  1. 在端点内部,我们要做的第一件事是将地址存储在一个变量中:
app.get('/address/:address', function(req, res) {
       const address = req.params.address;
});
  1. 我们要做的下一件事是使用getAddressData方法获取给定地址的所有数据。为了做到这一点,我们将在端点中添加以下突出显示的代码:
app.get('/address/:address', function(req, res) {
       const address = req.params.address;
       bitcoin.getAddressData(address);
});
  1. 通过这个方法,我们得到一个返回给我们的对象,其中包含addressTransactionsaddressBalance。我们要将这些数据存储在一个变量中,如下所示:
app.get('/address/:address', function(req, res) {
       const address = req.params.address;
       const addressData = bitcoin.getAddressData(address);
});
  1. 最后,我们要返回包含这些数据的响应,如下所示:
app.get('/address/:address', function(req, res) {
       const address = req.params.address;
       const addressData = bitcoin.getAddressData(address);
       res.json({
 addressData: addressData
 }); 

});

这就是我们构建/address/:address端点的方式。现在,让我们测试这个端点,以确保它能正常工作。

测试/address/:address 端点

为了测试端点,我们需要向区块链添加一些交易数据,让我们按照以下步骤来做:

  1. 转到浏览器,探索localhost:3001上存在的区块链。你会发现这里只有一个区块。所以,让我们向其中添加更多的交易数据和区块。

  2. 要做到这一点,转到 Postman,并将交易数据发送到localhost:3001/transaction/broadcast。在创建这些交易时,我们要确保跟踪一个特定的地址,以便在测试/address/:address端点时进行检查。为了跟踪这个特定的地址,让我们将一个地址的前三个字母改为 JEN。

  3. 让我们创建第一笔交易。将"amount":值设置为100,并在此交易的发送者地址中添加JEN

  1. 然后,点击发送,将交易发送到节点3001。然后,按照类似的步骤,为amount: 200进行另一笔交易,这次将JEN添加到接收者的地址,并将发送者的地址保持为随机哈希值:

  1. 现在,挖掘一个区块,将这些交易添加到区块链中。转到localhost:3001/mine,并按照以下方式在链中挖掘一个新的区块:

同样地,你可以通过改变金额值和交换发送者和接收者的地址来进行更多的交易,其中地址中包含JEN。一旦创建了一些交易,就挖掘一个区块,将这些新交易添加到区块链中。然后,再次创建新的交易,并通过交换发送者和接收者的地址给它们不同的金额。再次挖掘一个新的区块,将交易添加到区块链中。

然后,通过访问localhost:3001/blockchain来探索整个区块链,其中包括我们添加的新交易和区块。你将看到一堆区块和区块链内的交易。

现在,为了测试/address/:address端点,让我们按照以下步骤进行:

  1. 转到浏览器,在新标签页中输入localhost:3001/address/端点。

  2. 然后,从我们刚刚添加到区块链中的交易中复制一个地址,并将其粘贴到端点中。参考下面的截图:

  1. 现在,当我们运行这个端点时,我们应该看到与该特定地址相关的所有交易,以及该特定地址的比特币余额。看一下下面的截图:

在上面的截图中,我们得到了addressData属性的返回,其中包括addressTransactions数组和addressBalance属性。addressTransactions数组包括与我们在端点中提到的地址相关的所有交易。此外,addressBalance属性包括我们在端点中提到的地址的比特币余额:

  1. 接下来,你可以尝试通过复制挖矿奖励交易的接收者地址,并将其粘贴到/address/:address端点中,来检查节点地址的余额,就像我们在上一个例子中所做的那样。

  2. 运行这个端点后,你将看到挖矿奖励交易的余额。尝试实现许多其他类似的例子,以更清楚地了解/address/:address端点的工作原理。

  3. 另一个你可以尝试实现的例子是传递一个在区块链中不存在的地址。你将会得到以下返回的响应:

从前面的截图中,我们可以观察到addressTransactions数组为空,因为与我们输入的不存在的地址相关联的交易不存在。此外,不存在地址的addressBalance值为0。因此,我们可以从测试中得出结论,即/address/:address端点的工作方式正如它应该。

添加区块浏览器文件

让我们了解如何设置区块浏览器前端。区块浏览器将是一个用户界面,我们可以通过浏览器与区块链进行交互。为了构建这个用户界面并使其功能正常,我们需要使用 HTML、CSS 和 JavaScript。

现在,您不必自己构建所有的前端,您可以在以下链接找到一个完整的预构建前端:github.com/PacktPublishing/Learn-Blockchain-Programming-with-JavaScript/blob/master/dev/block-explorer/index.html。我们在本节中没有构建整个前端,因为这不是本书的重点。

要构建前端,您只需复制提供的链接中的文件并将其添加到项目的文件结构中。现在,转到dev文件夹并在其中创建一个名为block-explorer的新文件夹。在这个block-explorer文件夹内,创建一个名为index.html的文件,然后将提供的前端代码粘贴到其中并保存文件。您将在下一节中快速了解这个前端代码包含什么以及代码在哪里起作用。

构建/block-explorer端点

让我们构建一个端点,用于检索block-explorer文件:

  1. 转到dev/networkNode.js文件,在这里,创建一个新的端点,将向我们发送这个文件。定义端点如下:
app.get('/block-explorer', function(req, res) {

});
  1. 现在,在这个端点内,我们想做的就是将index.html文件发送回给调用这个端点的人:
app.get('/block-explorer', function(req, res) {
    res.sendFile('./block-explorer/index.html', { root: __dirname });
});

在前面的部分中,您可能已经注意到我们通常使用res.json,这是发送 JSON 数据的一种方式。然而,在这个端点中,我们想要发送整个文件,所以我们将使用res.sendFile方法。请注意,在前面的代码中,我们使用了{ root: __dirname }。这段代码表示我们应该查看项目存储的目录,并在其中查找具有/block-explorer/index.html路径的文件。这就是为什么我们将此选项作为第二个参数添加到端点中的原因,也是我们如何构建一个发送index.html文件的端点。

  1. 接下来,保存networkNode.js文件,并通过在浏览器中访问localhost:3001/block-explorer来验证这个端点是否有效。然后,您将看到区块浏览器的前端,如下所示:

您在这个前端中看到的所有内容都包含在我们刚刚创建的index.html文件中。

区块浏览器文件说明

在本节中,我们将简单地浏览一下我们在上一节中创建的index.html文件。我们将这样做是为了更好地理解发生了什么。所以,让我们开始吧。

index.html文件中,我们有所有的 HTML 和 JavaScript 代码,为区块浏览器提供必要的功能。这段代码还允许我们访问 API,最后,我们只是有一些 CSS 和样式,使一切在浏览器中看起来很好。

代码首先导入了一些库,比如angular.js,用于访问 API,还有 jQuery、Bootstrap 和一些 Bootstrap 样式,使一切功能正常且美观:

<head>
  <title>Block Explorer</title>
  <script src="img/angular.min.js"></script>
  <script src="img/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
  <script src="img/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
  <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>

接下来,我们有 HTML 模型的主体,其中包括区块浏览器的标题:

<body ng-app="BlockExplorer">
  <div class="container" ng-controller="MainController">
    <div class="row">
      <div class="col-md-8 offset-md-2">
        <h1 id="page-title">Block Explorer</h1>
      </div>
    </div

然后,我们有一个文本输入表单:

<div class="row">
      <div class="col-md-6 offset-md-3">
        <form ng-submit="search(searchValue)">
          <div class="form-group">
            <input type="text" class="form-control" ng-model="searchValue">
          </div>

接下来,我们有一个select输入,其中包含三个选项:区块哈希交易 ID地址

<div class="form-group">
        <select class="form-control" ng-model="searchType">
                <option value="block">Block Hash</option>
                <option value="transaction">Transaction ID</option>
                <option value="address">Address</option>
        </select>
</div>

要使用此页面,让我们在文本字段中输入块哈希、交易 ID 或地址,然后从下拉菜单中选择我们要查找的内容,如下截图所示:

最后,在 HTML 代码中,一旦我们从区块链中获得了一些数据,我们只需有一些表格来显示所有的数据。

此外,我们的index.html文件中还有一些 JavaScript 代码。在这个 JavaScript 代码中,我们使用 Angular 来调用我们的 API:

 window.app = angular.module('BlockExplorer', []);
 app.controller('MainController', function($scope, $http) {
          $scope.block = null;
          $scope.transaction = null;
          $scope.addressData = null;
          $scope.initialSearchMade = false;

然后我们有一个方法,当我们选择“块哈希”选项时,我们会命中/block/:blockHash端点:

$scope.fetchBlock = function(blockHash) {
        $http.get(`/block/${blockHash}`)
        .then(response => {
          $scope.block = response.data.block;
          $scope.transaction = null;
          $scope.addressData = null;
        });
      };

同样,我们还有/transaction/:transactionId端点的方法:

$scope.fetchTransaction = function(transactionId) {
        $http.get(`/transaction/${transactionId}`)
        .then(response => {
          $scope.transaction = response.data.transaction;
          $scope.block = null;
          $scope.addressData = null;
        }); 
      };

我们还有/address/:address端点的方法:

$scope.fetchAddressData = function(address) {
        $http.get(`/address/${address}`)
        .then(response => {
          $scope.addressData = response.data.addressData;
          if (!$scope.addressData.addressTransactions.length) $scope
            .addressData = null;
          $scope.block = null;
          $scope.transaction = null;
        }); 
      };

在接下来的 JavaScript 代码中,我们只有一点点更多的功能,然后在代码的最后有 CSS 样式。因此,这段代码包含在index.html文件中。如果您想深入了解,以获得更清晰的理解,可以随意这样做。您也可以根据自己的喜好进行自定义。

然后点击搜索,如果指定的数据存在于区块链中,将显示一个表格,其中将显示所有这些数据。如果我们的区块链上不存在数据,您将得到未找到数据的结果。这就是区块浏览器前端的工作原理。

到目前为止,我们已经构建了一个完整的区块浏览器前端,并且我们有区块浏览器的后端——我们刚刚创建的三个端点,以便搜索整个区块链。

在下一节中,我们将测试区块浏览器,以确保它完美地工作。

测试我们的区块浏览器

在这一部分,我们将测试区块浏览器,以确保其正常工作,并确保我们在上一章中创建的所有端点和方法也能正常工作。如果区块浏览器正常工作,那么我们已经知道整个区块链也在去中心化的区块链网络上正常运行,所以当我们进入本章的最后一部分时,一切都很顺利地结束了。因此,这是我们将要进行的最后一次测试。现在让我们按照以下步骤来测试区块浏览器:

  1. 为了测试区块浏览器,我们应该确保我们有五个节点都在运行。

  2. 接下来,转到浏览器,通过localhost:3003/block-explorer打开区块浏览器。实际上,您可以转到网络中任何一个节点上托管的区块浏览器,因为整个区块链是托管在整个网络上的。

  3. 现在,为了测试区块浏览器,我们需要向区块链添加一些数据。要向区块链添加数据,我们只需创建大量交易并创建一些新的区块,类似于我们在前几节中所做的。您可以参考前几章,快速回顾如何向区块链添加交易和区块。

  4. 在添加数据之后,我们现在可以测试区块浏览器。让我们首先通过搜索块哈希来获取一个块。让我们选择“块哈希”选项:

  1. 然后,从区块链中复制任何一个块的哈希值,并将其粘贴到区块浏览器中:

  1. 现在,点击搜索按钮。您应该看到与以下截图中类似的输出:

这基本上是区块浏览器的工作原理。我们输入我们正在寻找的哈希或数据片段,作为回报,我们得到该数据片段作为输出。从前面的屏幕截图中,我们可以观察到,我们输入到区块浏览器的哈希值返回了索引为4的区块。我们还得到了与该区块相关的所有细节。此外,您可能已经注意到,对于此搜索,我们正在命中/block/:blockHash端点。

  1. 接下来,通过输入transactionId搜索交易。转到区块浏览器并选择交易 ID 选项。然后,转到区块链并从任何区块中复制一个transactionId值,并将其输入到区块浏览器:

  1. 然后点击搜索按钮。您将看到类似以下的输出:

从前面的屏幕截图中,我们可以看到我们得到了与我们输入到区块浏览器的transactionId相关的所有交易细节。我们还得以观察到该特定transactionId的比特币余额为 400 比特币。

  1. 最后,测试地址端点。要做到这一点,从区块浏览器中选择地址选项,然后输入任何一个区块中的发件人或收件人地址。然后点击搜索按钮。您应该在屏幕上看到以下输出:

从前面的屏幕截图中,我们可以看到该地址有 749.35 比特币的余额,并且我们可以看到与我们输入的地址相关的所有交易。

现在,对于这些搜索中的任何一个,如果我们输入一个不存在的数据片段,我们将得到以下结果:

这证明了区块浏览器的工作原理与应有的一样。

总结

在本章中,我们构建了一个令人惊叹的用户界面,用于探索本书中构建的区块链。我们首先定义了查询所需数据的必要端点。然后,我们构建了诸如getBlockgetTransactiongetAddressData之类的方法,以帮助端点查询数据。此外,我们开发了/block/:blockHash/transaction/:transactionId/address/:address端点。在做完这些之后,我们将区块浏览器的前端代码添加到我们的区块链目录中,然后测试了区块浏览器和我们开发的所有端点。

通过本章,我们已经到达了本书的结尾。到目前为止,我们已经构建了自己的区块链,并为其添加了所有必要的功能。除此之外,我们还建立了我们自己的去中心化网络,并建立了一个用于探索区块链的界面。

下一章将是对本书中所学内容的快速总结。然后,我们将探索我们已开发的区块链还可以做些什么。

第八章:总结...

欢迎来到本书的最后一章。到目前为止,你已经对区块链功能及其构建有了深入的了解。你一定对构建自己的区块链并探索其各种功能感到兴奋!

在我们结束本书之前,让我们快速回顾一下我们到目前为止学到的东西,并探讨我们可以对我们的区块链进行哪些改进或修正,使其更安全可靠。

所以,让我们准备迈出最后的步伐...

快速回顾

随着上一章的完成,我们已经建立好了我们的区块链。如果你想想你在整本书中取得了多少成就,那是相当令人印象深刻的。

我们从零开始,建立了一个区块链数据结构,然后创建了一个与之交互的 API。然后我们将我们的 API 转变为一个去中心化的区块链网络,并在多个不同节点上同步了整个网络的数据。

然后,我们创建了一个共识算法,以确保所有节点上的数据是同步和合法的。最后,我们建立了一个区块浏览器,通过用户界面来探索我们的区块链。在整本书中,我们建立了许多功能,以及一个令人印象深刻的区块链原型。

你必须意识到的一件重要的事情是,在整本书中,我们一直在一台计算机上运行我们的五个节点。

然而,如果你将整个项目下载到多台不同的计算机上,你就可以在每台计算机上运行每个节点,真正模拟去中心化的区块链网络是如何工作的。

你只需要确保每台运行你的项目的计算机在同一个网络上。你不再需要本地主机地址,而是需要每个节点运行的 IP 地址。

尝试尝试这个想法。看到你的区块链网络在多台不同的计算机上运行是非常酷的。

改进的领域

现在,有一些可以改进的地方。其中之一是错误处理。在整本书中,我们并没有做太多的错误处理,因为我们专注于确保实际功能的正确运行,但为了改进这个区块链,我们肯定需要做很多错误处理。这将确保我们的区块链不会被黑客攻击,并确保它的正确运行。

另一个可以改进这个区块链的领域是在进行交易时。目前,当我们进行交易时,我们从一个人发送一定数量的比特币给另一个人,但我们从来没有验证发送方是否真的有这些比特币可以发送。

我们鼓励你尝试一下我们区块链数据结构中的createTransaction方法,并找到一种验证发送方是否真的有足够比特币发送的方法。这对你来说是一个很好的实践项目。你需要获取发送方当前的比特币余额,并进行验证,确保他们有足够的比特币进行交易。

这个区块链的另一个改进方式是将其打造成类似以太坊的去中心化应用平台。你只需要向区块链添加一个功能,允许你在每个区块内存储更多的数据,可以存储不同类型的数据,而不仅仅是交易。例如,你可以在每个区块内存储用户数据、管理数据或任何其他类型的数据。然后在生成每个区块的哈希时使用这些数据,就像我们在生成交易时使用哈希一样。

我们建立的区块链有很多定制和改进的方式;然而,现在,我们有一个完全功能的原型区块链,可以托管在去中心化的区块链网络上。

所以,这就是我们的全部内容。我们希望你喜欢这本书,学到了很多东西,并且在构建我们创建的区块链时玩得开心!

posted @ 2024-05-23 15:55  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报