首页 > 其他分享 >bc-etp-merge-0

bc-etp-merge-0

时间:2024-05-01 15:45:21浏览次数:16  
标签:function etp bc 我们 merge 使用 web3 区块 节点

面向企业的区块链教程(全)

原文:zh.annas-archive.org/md5/71bd99f39f23fd60e3875318ad23711a

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

区块链正在迅速增长,并改变着商业的运作方式。领先的组织已经在探索区块链的可能性。通过本书,你将学会如何构建端到端的企业级去中心化应用程序DApps)并在组织中扩展它们以满足公司的需求。

本书将帮助你了解什么是 DApps 以及区块链生态系统的运作方式,并通过一些实际的例子来说明。这是一本全面的端到端书籍,涵盖了区块链的各个方面,例如它对于企业和开发人员的应用。它将帮助你了解流程,以便你可以将其纳入到你自己的企业中。你将学会如何使用 J.P.摩根的 Quorum 构建基于区块链的应用程序。你还将介绍如何编写能够在企业区块链解决方案中通信的应用程序。你将学会编写无需审查和第三方干预即可运行的智能合约。

一旦你对区块链有了很好的理解,并学习了关于 Quorum 的一切,你就会开始构建真实世界的区块链应用,应用于支付和货币转移、医疗保健、云计算、供应链管理等领域。

本书适合谁

本书适合创新者、数字化变革者和想要使用区块链技术构建端到端 DApps 的区块链开发人员。如果你想要在企业范围内扩展你现有的区块链系统,你也会发现这本书很有用。它为你提供了解决企业实际问题所需的实用方法,结合了理论和实践的方法。

如何充分利用本书

你必须具备 JavaScript 和 Python 编程语言的使用经验。

你必须之前开发过分布式网络应用。

你必须了解基本的加密概念,如签名、加密和哈希。

下载示例代码文件

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

你可以通过以下步骤下载代码文件:

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

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

  3. 点击“代码下载 & 勘误”。

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

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

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Blockchain-for-Enterprise。 如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有来自我们丰富的图书和视频目录的其他代码包可供使用,位于github.com/PacktPublishing/。快来看看吧!

使用的约定

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

CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。例如:"在使用raft.addPeer添加节点时,将出现此 Raft ID。"

代码块设置如下:

url = "http://127.0.0.1:9002/"
port = 9002
storage = "dir:./cnode_data/cnode2/"
socket = "./cnode_data/cnode1/constellation_node2.ipc"
othernodes = ["http://127.0.0.1:9001/"]
publickeys = ["./cnode2.pub"]
privatekeys = ["./cnode2.key"]
tls = "off"

任何命令行输入或输出均如下所示:

git clone https://github.com/jpmorganchase/quorum.git
cd quorum
make all

粗体:表示一个新术语,一个重要词或者你在屏幕上看到的词。例如,菜单中的词或对话框中的词在文本中显示为这样。示例:"现在选择一个文件,输入所有者的名字,然后点击 提交。 ."

警告或重要说明会出现在这里。

提示和技巧会出现在这里。

联系我们

我们的读者的反馈总是受欢迎的。

一般反馈:通过电子邮件向[email protected]发送邮件,并在主题中提及书名。如果您对本书的任何方面有疑问,请通过电子邮件与我们联系[email protected]

勘误:尽管我们已尽一切努力确保内容的准确性,但错误确实会发生。如果您在本书中发现错误,我们将不胜感激,如果您报告给我们。请访问www.packt.com/submit-errata,选择您的书,点击勘误提交表单链接,然后输入详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激,如果您能向我们提供位置地址或网站名称。请通过电子邮件与我们联系[email protected] 并附上材料链接。

如果您对成为作者感兴趣:如果您有某个专业知识,并且有兴趣撰写或为一本书做出贡献,请访问authors.packtpub.com

评论

请留下评论。在您阅读并使用本书之后,为什么不在您购买它的网站上留下评论呢?潜在的读者可以看到并使用您的客观意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢!

欲了解更多有关 Packt 的信息,请访问packt.com

第一章:什么是去中心化应用程序?

从互联网诞生以来,所有开发的基于互联网的应用程序都基于客户端-服务器架构,其中有一个中心化的服务器构成应用程序的后端并控制整个应用程序。这些应用程序经常出现一系列问题,比如存在单一故障点,无法阻止网络审查,缺乏透明度,用户不相信他们的数据、活动和身份隐私等。这种中心化的架构甚至使构建某种应用程序成为不可能。例如,你不能使用这种架构构建数字货币。由于这些问题,出现了一种新的架构类型,称为去中心化应用程序DApps)。在本章中,我们将学习关于 DApps 的知识。

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

  • 什么是 DApps?

  • 去中心化、中心化和分布式应用程序之间有什么不同?

  • 什么是区块链?

  • 公共 DApps 和权限 DApps 之间的区别是什么?

  • 某些流行的联合 DApps 的示例及其工作原理

  • 用于构建企业 DApps 的各种流行平台是什么?

什么是 DApp?

DApp 是一种其后端运行在去中心化对等网络上,并且其源代码是开源的应用程序。网络中没有一个单一的节点完全控制 DApp。记住,当我们说一个应用程序是去中心化的,我们指的是技术上是去中心化的,但治理可以是分布式的、去中心化的或中心化的。

区块链应用程序(DApps)的主要优势在于它们没有单一的故障点,并且可以防止审查。 DApps 确实有一些缺点:一旦部署,很难修复错误或添加功能,因为网络中的每个人都必须更新他们的节点软件,而且将不同的 DApps 耦合在一起非常复杂,因为与集中式应用程序相比,它们很难构建,并涉及非常复杂的协议。

要使用 DApp,您首先需要运行 DApp 的节点服务器,以便您可以连接到对等网络。然后,您需要一个与 DApp 对应的客户端,该客户端连接到节点服务器并公开用于使用 DApp 的 UI 或命令行界面。

目前,DApps 在性能和可扩展性方面还没有像集中式应用程序那样成熟。在这些主题上仍然存在大量研究和开发,如性能、可扩展性、用户身份、隐私、DApp 之间的通信、数据冗余等。一个用例可能适用于 DApp,但基于当前可用技术使用例可以投入生产可能是一个挑战。去中心化应用程序的流行示例包括 Torrent,Bitcoin,Ethereum,Quorum 等。

一个 DApp 可以是公开的或许可的。公开的 DApp 是任何人都可以参与的,换句话说,它们是无需许可的,而许可的 DApp 是不是每个人都能加入的,所以你需要许可才能加入。当 DApp 的参与者是企业和/或政府实体时,许可的 DApp 被称为联盟 DApp。同样地,当许可的 DApp 的参与者只是企业时,我们可以称之为企业 DApp。在这本书中,我们将学习关于许可的 DApp 的一切。

你刚刚对分布式应用有了基本介绍,你一定想知道分布式应用和去中心化应用的区别是什么。好吧,当一个应用分布在多个服务器上时,就说这个应用是分布式的。去中心化应用默认情况下是分布式的,而中心化应用可能是分布式的也可能不是。中心化应用通常分布在多个服务器上,以防止停机,并处理大量数据和流量。

什么是区块链?

在我们讨论什么之前,我们需要理解什么是账本。在计算机科学中,账本是存储交易的软件。数据库与账本不同,因为在数据库中我们可以添加、删除和修改记录,而在账本中我们只能追加而不能删除或修改。

区块链基本上是一个实现去中心化账本的数据结构。区块链是相互连接的块链。每个块包含一系列交易和某些其他元数据,比如它是何时创建的,它的前一个块是什么,块号是多少,谁是块的创建者等等。每个块都维护前一个块的哈希值,因此创建了相互链接的块链。网络中的每个节点都应该保存完整的区块链副本,当一个新节点加入时,它将从其他节点请求并下载区块链。

像区块链这样的技术被称为分布式账本技术DLT)。DLT 是在许多地点、国家和/或机构之间复制、共享和同步数字交易的过程。你可以将区块链看作是一种 DLT。此外,并不是每个 DLT 系统都必须是去中心化的。在这本书中,我们只学习构建基于区块链的去中心化应用。

使用区块链的主要优势是它使交易无需中央信任方进行 facilitation;数据使用密码学进行安全保护,并且数据是不可变的,因为区块链消除了摩擦并降低了风险,因此结算实时发生,等等。基本上,它自动化了审计,使应用程序透明化,并提供了唯一的真相来源。

在现实世界中,私有区块链被用于贸易金融、跨境支付、数字身份、代币化和数字资产的结算与清算、产品所有权溯源、关键数据记录、签订合同、多方汇总(即它们可以用作共享的行业信息主库,允许成员查询数据)、支付与支付或支付与交付等等。

每个区块链节点维护一个包含区块链状态的数据库。状态包含运行区块链中所有交易的最终结果。例如,在区块链中,状态表示所有地址的最终余额。因此,当您查询区块链节点的地址余额时,它不必浏览所有交易并计算地址的最终余额;相反,它直接从区块链的状态中获取余额。比特币使用 LevelDB 来维护区块链的状态。即使数据库损坏,也可以通过简单地运行区块链中的所有交易来恢复数据库。

理解拜占庭容错

拜占庭容错BFT)是分布式系统的一个特性,表示它能容忍拜占庭失败。崩溃故障是指节点停止做任何事情(根本没有消息),而拜占庭失败是指节点根本不做任何事情或展示任意行为。基本上,拜占庭失败包括崩溃故障。

在任何使用区块链数据结构的去中心化计算环境中,都存在一个或多个恶意或不可靠参与者可能导致环境解散的风险。如果服务器集群中的几台服务器不能以一致的方式传递数据给其他服务器,那么服务器集群将无法正常工作。为了可靠,去中心化计算环境必须以一种方式设计,即它有解决这类拜占庭失败的解决方案。

在基于区块链的去中心化应用中,按定义没有中央权威,因此使用一种称为共识协议的特殊协议来实现 BFT。

简单来说,你一定想知道如何确保每个人都有相同的区块链副本,以及当两个节点发布不同的区块链时如何确定哪个区块链是正确的?此外,在分布式架构中没有主节点的情况下,如何决定谁创建区块?好吧,共识协议为这些问题提供了答案。共识协议的几个例子包括工作量证明PoW)、权益证明PoS)、权威证明PoA)、PBFT 等等。

共识协议专为许可或公共区块链设计。为公共区块链设计的共识协议在实现为许可区块链时可能会产生安全性和性能问题。每个共识协议都有不同的性能和可扩展性向量。在为基于区块链的 DApp 选择共识协议时必须保持警惕。

Raft 和 Paxos 等共识协议不是 BFT;相反,它们只是使系统具有崩溃容错性。因此,在选择共识协议时,您也应考虑这一点。

您可能已经听说过 PoA 这个术语。 PoA 是共识协议的一种分类,在其中有一组权限节点,这些节点明确被允许创建新的区块并保护区块链。 Ripple 的迭代过程,PBFT,Clique,Aura 等都是基于 PoA 的共识协议的例子。

用户账户的表示

在基于区块链的应用程序中,用户账户使用非对称密钥对进行标识和认证。私钥用于代表用户签署交易。基于用户名和密码的账户系统在区块链上无法工作,因为它无法证明哪个用户发送了交易。使用私钥-公钥对的缺点包括它们不用户友好,如果你丢失了私钥则无法恢复它。因此,它为用户增加了一个新的责任,即保护他们的私钥。用户账户的地址在区块链上充当账户标识符。用户账户的地址是由公钥派生的。

什么是 UTXO?

一些区块链应用程序使用 UTXO 模型进行交易。比特币和 MultiChain 等区块链应用程序使用此模型。即使像 R3 Corda 这样的分布式账本技术也使用此模型。让我们通过理解比特币交易的工作方式来理解这个模型。

在比特币中,一个交易是零个或多个输入和输出的集合。这些输入和输出对象被称为未使用的交易输出UTXO)。交易的输出被用作未来交易的输入。一个 UTXO 只能被使用一次。在比特币中,每个 UTXO 包含一个面额和一个所有者(比特币地址)。在这个模型中,未使用的 UTXO 中的地址余额被存储。要使交易有效,必须满足以下要求:

  1. 交易必须包含对每个消耗的 UTXO 的所有者的有效签名

  2. 所消耗的 UTXO 的总面额必须等于或大于所生产的 UTXO 的总面额

用户的余额被计算为他们拥有的 UTXO 的面额总和。交易可以消耗零个或多个 UTXO 并产生零个或多个 UTXO。为了向矿工支付奖励,它在区块中包含一个交易,该交易消耗零个 UTXO,但产生一个 UTXO,其面额分配给它应该授予自己的比特币数量。

当区块链交易涉及资产转移时,UTXO 事务模型是合适的,但对于无资产转移交易(如记录事实、调用智能合约等),这种模型就不合适了。

流行的许可制区块链平台

现在,我们已经基本了解了什么是 DApp、区块链和 DLT,让我们来概述一下可用于构建许可制区块链应用程序和 DApp 的平台。我们只会介绍市场上流行的、有需求的平台。

以太坊

以太坊是继比特币之后最受欢迎的 DApp。以太坊是一个分散的平台,允许我们在其上构建其他基于区块链的 DApp。在以太坊中,我们使用以太坊智能合约来构建 DApp。智能合约是一种完全按程序运行的应用程序,没有任何停机时间、审查、欺诈或第三方干预的可能性。以太坊可以看作是一个部署和运行智能合约的平台。以太坊支持两种共识协议,即 PoW 和 PoA(Clique)。

主要的以太坊公共网络使用 PoW 进行共识。如果您想部署自己的私有以太坊网络,那么您必须使用 PoA。PoW 需要大量的计算能力来保护区块链的安全,因此适用于公共区块链使用,而 PoA 则没有任何这样的计算能力要求;相反,它需要网络中的少数权威节点来达成共识。

你一定在想为什么我们需要智能合约来构建 DApp。为什么不能简单地在区块链上放置格式化消息,以交易的形式在客户端上解释它们呢?好吧,使用智能合约可以为你带来技术和商业上的双重利益。

Quorum

Quorum 是一个分散的平台,允许我们在其上构建基于许可制区块链的 DApp。实际上,Quorum 是以太坊的一个分叉(实际上 Quorum 是以太坊的一个分叉,使用 Golang 实现的以太坊的一个实现),因此如果你曾经在以太坊上工作过,那么你将会发现学习并使用 Quorum 来构建许可制区块链是很容易的。许多企业选择 Quorum 来构建区块链,因为以太坊拥有庞大的社区,这使得找到以太坊开发人员变得容易。Quorum 与以太坊的不同之处在于,它支持隐私(让各方可以私下进行交易)、对等节点白名单,因此可以指定允许连接到您的节点的其他节点列表(在以太坊中,这需要在网络级别完成),适用于许可制区块链的许多不同类型的共识协议,并提供非常高的性能。

Quorum 目前支持三种共识协议,QuorumChain、IBFT 和 Raft。在本书中,我们将跳过 QuorumChain,因为 Raft 和 IBFT 满足了我们的所有需求。

Microsoft Azure 提供了 BaaS,可以在云上轻松构建自己的 Quorum 网络。但是,在本书中,我们将学习如何手动安装,并不会使用 BaaS。

奇偶校验

以太坊的流行节点软件包括 Go Ethereum、以太坊 C++ 和 Parity。Parity 还支持两种与以太坊的 PoW 不同的共识协议,专为权限区块链设计。这些共识协议是 Aura 和 Tendermint。许多以太坊开发者在不需要 Quorum 提供的额外功能时,会使用 Parity 而不是 Quorum。

由于 Parity 与 Quorum 没有提供任何独特的功能,所以我们将在本书中跳过 Parity。但是,一旦你完成了这本书,你会发现很容易掌握 Parity 的概念,并且也能够利用它构建一些东西。

MultiChain

MultiChain 是一个构建基于权限的区块链 DApps 的平台。MultiChain 的独特功能包括权限管理、数据流和资产。它不支持智能合约。这是一个构建基于区块链的 DApps 的非智能合约平台的例子。MultiChain 使用循环验证共识。

最初,MultiChain 的理念是在区块链上管理资产的所有权和转移。对资产的操作包括发行、再发行、转移、原子交换、托管和销毁。后来,数据流被引入以提供在 MultiChain 中表示数据的不同方式。在 MultiChain 中可以创建任意数量的流,每个流都是独立的追加集合。关于流的操作包括创建流、写入、订阅、索引和检索。因此,基本上,MultiChain 上的区块链用例可以建立在资产或流的基础上。最后,权限管理用于控制谁能连接、交易、创建资产/流、挖掘/验证和管理。

MultiChain 提供与比特币生态系统最大的兼容性,包括点对点协议、交易/区块格式、UTXO 模型和比特币核心 API/运行时参数。因此,在开始学习 MultiChain 之前,最好至少了解比特币的工作原理。

Hyperledger Fabric 1.0

在介绍 Hyperledger Fabric 1.0 之前,我们需要明确了解 Hyperledger 的概念。Hyperledger 是由 Linux 基金会于 2015 年 12 月发起的一个开源区块链和相关工具的整体项目。在撰写本书时,Hyperledger 有四个项目:Fabric、Sawtooth、Iroha 和 Burrow。

Hyperledger Fabric 是 Hyperledger 下最受欢迎的项目。IBM 是该项目的主要贡献者。IBM 的 Bluemix 还提供了在云上构建自己的 Fabric 网络的 BaaS 服务。

Hyperledger Fabric 1.0 是一个构建自己的基于区块链的权限管理应用的平台。当前,在撰写本书时,Hyperledger Fabric 1.0 仅支持分布式架构,并且对于区块的创建,它依赖于一个名为orderer的中心可信节点。它支持智能合约、网络许可、隐私和其他功能。在 HLF 1.0 中,有一种特殊的节点称为OSN,由可信方托管。这个 OSN 创建区块并分发给网络中的对等方。由于信任这个节点,没有必要进行共识。HLF 1.0 目前支持 CouchDB 和 LevelDB 来存储区块链的状态。网络中的对等方默认在 LevelDB 数据库中存储区块链的状态。

HLF 1.0 通过频道的概念实现隐私。频道是网络中的子区块链,并且允许根据配置让某些参与方成为某个频道的一部分。实际上,每个交易都必须属于一个频道,当部署 HLF 1.0 网络时,默认会创建一个默认频道。OSN 可以看到所有频道中的所有数据,因此它应该是一个可信的方。从技术上讲,如果你不能信任单个方处理所有频道,可以配置网络来拥有托管不同频道的多个 OSN。即使流量庞大或者 OSN 的可用性至关重要,也可以将 Kafka 连接到 OSN 以获得更好的性能和更高的稳定性。如果需要高可用性,我们甚至可以每个频道连接多个经过 Kafka 连接的 OSN。

Fabric 1.0 有一个名为交易背书的特性,提供了在发送交易之前从特定方批准的机制。当我们说一个成员在网络中背书了一笔交易时,我们的意思是该成员已经验证了交易。HLF 中的每个链码(智能合约)在部署时都有一个背书策略。该策略规定了哪些成员必须背书与该链码相关的交易。默认策略规定频道中的任一成员必须签署交易。但是,我们可以定义包含ANDOR运算符的自定义策略。

此外,同一频道的对等方无论是否存在 OSN,都会向彼此广播区块,但在缺乏 OSN 的情况下,无法为频道创建新的区块。对等方使用称为gossip 数据传播协议的特殊协议广播区块。

HLF 1.0 拥有非常先进的成员功能,可以控制网络成员,并且也内部使用于特定组织。在 HLF 1.0 中,您可以用 Java 或 Go 编程语言编写链码。将来,Fabric 1.0 将配备 Simple Byzantine Fault ToleranceSBFT)共识协议和其他一些功能,这将使我们能够构建 DApps。同样,还有各种新功能正在开发中,并将作为产品的子版本在将来发布。

构建您的第一个 HLF 1.0 应用程序的最佳方式是查看 github.com/hyperledger/fabric-samples 上的示例,并根据您的应用程序需求进行修改。您可以在 hyperledger-fabric.readthedocs.io/en/latest/ 找到 HLF 1.0 的详细文档。

BigchainDB

BigchainDB 是一个使用区块链的分布式数据库。BigchainDB 具有高度可扩展性和可定制性。它使用区块链数据结构。它支持诸如丰富的权限、PB 级容量、高级查询、线性扩展等功能。在编写本书时,BigchainDB 尚未达到生产就绪,但可用于构建概念验证PoCs)。我们将在后面的章节中学习它的工作原理,并使用它创建一个基本的 PoC。

星际文件系统

星际文件系统IPFS)是一个分布式文件系统。IPFS 使用 分布式哈希表DHT)和 Merkle 有向无环图DAG)数据结构。它使用类似于 Torrent 的协议来决定如何在网络中移动数据。IPFS 的一个高级功能是它支持文件版本控制。为了实现文件版本控制,它使用类似于 Git 的数据结构。

尽管它被称为分布式文件系统,但它并不遵循文件系统的一个主要属性,即,当我们将某物存储在文件系统中时,应该一直存在,直到删除。但是,IPFS 的工作方式并不是这样的。每个节点并不存储所有文件,而是仅存储它需要的文件。因此,如果一个文件不受欢迎,那么许多节点将不会拥有该文件,因此文件在网络中消失的可能性很大。由于这个原因,我们可以称 IPFS 为分布式点对点文件共享应用程序。我们将在后面的章节中了解它的工作方式。

Corda

Corda 是一个用于构建自己的基于权限的 DLT 应用程序的平台。Corda 是 R3 的产品。R3 是一个与超过 100 家银行、金融机构、监管机构、贸易协会、专业服务公司和技术公司合作的企业软件公司,致力于开发 Corda。Corda 的最新版本是 1.0,旨在取代用于金融交易的传统软件,并使组织能够数字化使用传统软件系统繁琐的各种业务流程。

上述图表显示了 Corda 网络的高级架构。让我们从高层次了解 Corda 的架构。R3 的 Corda 的想法是为金融交易提供共享可信赖的分类账。R3 的 Corda 不是一个区块链平台,因此没有区块、全局广播等概念。所有交易都是点对点的。Corda 应用程序不是去中心化的。在 Corda 中,智能合约称为CorDapps,它们是用 Java 或 Kotlin 编写的。

基础设施服务形成了网络中的节点,应由信任方承载。网络地图发布了所有其他节点的 IP 地址,以便节点可以联系其他节点。权限服务为节点提供加入网络的权限;如果允许加入网络,则节点将从网络的权限服务收到根权威签名的 TLS 证书。记帐员提供交易排序和时间戳服务(可选地,记帐员还充当时间戳授权,验证交易在记帐之前发生在特定时间窗口内)。记帐服务可以是单个网络节点、一组互相信任的节点集群或一组互相不信任的节点集群。

预期由网络不信任的企业承载记帐员,因此记帐员之间需要达成共识,因此 Corda 提供了各种可插拔的共识协议,如 Raft、BFT 等。

有时,Corda 应用需要依赖外部应用程序 API。例如,使用 Corda 构建的多币种银行间支付应用程序需要获取汇率。在这种情况下,发起交易的节点可以获取汇率并放入交易中,但你如何信任该节点?另外,每个节点都不能简单地重新获取汇率以验证其是否正确,因为其他节点获取汇率时汇率可能已经发生变化,并且这也不是可扩展的解决方案。因此,Corda 提供了 Oracle 来解决这个问题。网络中可以有一个或多个 Oracle。Oracle 是作为两个应用程序之间通信的桥梁的服务。在 Corda 中,交易发起者可以从 Corda 网络外获取信息,并从Oraclize获取签名以证明其有效性。可选地,Oraclize 还可以根据请求向交易发起者提供信息。显然,Oraclize 应该由信任的方承载,关于他们提供和签名的信息。

Corda 支持任何可插拔的 RDBMS(当前正在使用 H2 数据库)来存储智能合约数据。数据隐私由哪些节点可以看到交易来维护。框架还提供了多重签名支持,这使得多个节点可以签署交易。Corda 的一个主要缺点是,由于没有全局广播,每个节点必须以传统方式维护自己的备份和故障转移冗余,因为网络中没有内置冗余。节点将存储交易并重试向接收者发送消息,直到接收者成功接收为止。一旦消息被接收,发送方就没有更多的责任。

交易的有效性

由于并非所有交易都广播给网络中的所有参与方,为了防止双重支付(双重支付是一种攻击 DLT,将相同的资金花费两次,转移相同的资产两次等),我们使用公证员。公证员包含所有未使用的 UTXOs,在公证之后,他们将其标记为已使用,并将新的未使用的 UTXOs 加入其状态。在发送给其他参与方之前,交易发起者会请公证员对交易进行公证。

只有在公证员先前签署了交易的输入状态时,公证员才能签署交易。但是,这并不总是情况,因此 Corda 还让我们改变状态的指定公证员。这种情况通常是由于以下原因导致的:

  • 消耗具有不同指定公证员的状态的交易

  • 一个节点希望使用不同的公证员以实现隐私或效率

在这些交易可以创建之前,状态必须首先被重新指向以使所有状态具有相同的公证员。这是通过特殊的改变公证的交易来实现的。

CorDapps 不像其他平台的智能合约那样有状态。它们的目的只是验证所产生的输出是否正确。每个 UTXO 指向一个 CorDapp。CorDapps 定义了 UTXOs 的格式。在一个交易中,我们可以有来自多个 CorDapps 的 UTXOs,在这种情况下,每个 CorDapp 只运行一次,并验证属于它的所有输入和输出。要使交易有效,它必须在合约上有效;CorDapp 应该批准它。

除了输入和输出,交易可能还包含命令,即平台本身不解密但有助于 CorDapps 处理输入和输出的小数据包。命令是与一些公钥关联的数据片段。命令用于向 CorDapps 提供它无法通过 UTXOs 获得的附加信息。在合同开始执行之前,平台确保交易由命令中列出的每个密钥签名。因此,CorDapp 可以相信所有列出的密钥已签署了交易,但需要验证预期的各方是否已签署。公钥可能是随机的或无身份的以保护隐私,也可能与知名的法律身份相关联。

Oracle 会以命令的形式向交易提出者提供已签名的信息,这些命令封装了特定的事实,并将 Oracle 列为必需的签名者。

交易也可以包含附件的哈希值。附件是 ZIP/JAR 文件。当存在大量数据片段可以在多个不同的交易中重用时,附件非常有用。

在验证提议的交易时,节点可能没有所需的交易链上的所有交易。因此,Corda 允许节点从提议者那里请求缺失的交易。交易提出者始终会拥有所需交易链的所有交易,因为在验证交易并创建提议的交易输入状态时,他们会请求这些交易。

最后,一旦交易提交,您可以查询 Vault(跟踪未消耗和已消耗的状态)。

要了解有关 Corda 的更多信息并构建您的第一个 Corda 应用程序,请访问 docs.corda.net/,其中包含详细的文档。您可以下载并尝试几个示例应用程序。

Hyperledger Sawtooth

Sawtooth 是一个用于构建自己的许可 DApps 的分散平台。 Sawtooth 的主要贡献者是英特尔。 Sawtooth 的特殊之处在于它使用了 受信任的执行环境 (TEE)(目前仅支持 Intel 的 SGX)进行共识,这使得网络非常安全和可信,并增加了对共识最终结果的信任。

TEE 是主处理器的安全区域。它保证了加载到内部的代码和数据在机密性和完整性方面的保护。 TEE 是一个隔离的执行环境,提供了诸如隔离执行、受信任应用程序的完整性以及其资产的机密性等安全功能。

已过时间的证明 (PoET) 是 Sawtooth 使用的共识协议的名称。在 PoET 中,有一种特殊类型的节点称为验证者。验证者必须在支持 SGX 的 CPU 上运行其节点。这就是 PoET 的工作原理。

每个验证器向隔离区(一个可信函数)请求等待时间。对于特定的交易块,等待时间最短的验证器被选为领导者。一个函数,比如 CreateTimer,为交易块创建一个定时器,这个定时器被保证是由隔离区创建的。另一个函数,比如 CheckTimer,验证定时器是否由隔离区创建,并且如果定时器已经过期,则创建一个可以用来验证验证器是否确实等待了规定时间后才声明领导地位的认证。PoET 在整个验证器群体中随机分配领导选举。选举的概率与贡献的资源成比例(在这种情况下,资源是具有 TEE 的通用处理器)。执行的认证提供了一个验证证书是在隔离区内创建的(以及验证器是否等待了规定的时间)的信息。此外,参与的低成本增加了验证器群体庞大的可能性,增加了共识算法的稳健性。

Sawtooth 也支持智能合约(具体来说,以太坊智能合约可以在 Sawtooth 上执行)。在性能方面,Sawtooth 在大量交易和节点方面都表现出色。

流行的区块链用例

让我们看一些许可区块链的流行用例。这将帮助我们了解企业可以使用许可区块链做什么以及什么用例适用于许可区块链。

Everledger

Everledger 是一个由区块链支持的钻石数字登记处。它是区块链上的供应链管理的一个例子。之所以使用区块链,是因为在区块链中,记录是不可变的。Everledger 使用了 40 多个特征,包括颜色和净度,来创建钻石的 ID。当这些信息被放置在区块链上时,这些信息就成为了一份记录宝石所有权的证书,从矿山到戒指。Everledger 已经将超过一百万颗钻石数字化,并与包括巴克莱在内的公司合作。区块链网络的参与者,如商户、银行和保险公司,可以验证钻石是否合法。Everledger 建立在 Hyperledger Fabric 平台上。未来,他们还计划将其他珍贵商品添加到他们的区块链中。

让我们举个例子场景,看看区块链如何在这个用例中发挥作用。Alice 购买了一颗钻石,对其进行了保险,并在 Everledger 区块链上注册了它。接下来,她丢失了钻石,并报告了失窃。保险公司随后对她进行了赔偿。最后,小偷 Bob 试图向珠宝商 Eve 出售被盗的钻石。她向 Everledger 请求验证,并发现这是一颗被盗的钻石。保险公司被通知有关被盗的钻石,并将其占有。

沃尔玛的食品追踪

沃尔玛的食品追踪用例是区块链和物联网的结合,使食品产品的历史透明化和可追溯到其来源。这是区块链上的供应链管理示例。沃尔玛的食品追踪供应链管理建立在 Hyperledger Fabric 平台之上。

每年有很多人死于食物中毒。一旦有人因食物中毒而生病或死亡,当局会试图追踪食物的来源,并确保源自该地的所有食品暂停销售并召回。这拯救了很多人的生命。但问题在于,由于供应链中的每个参与方都有自己的存储和检索信息的方式和过程,因此当局需要数周的时间来追踪来源,并阻止链中的每个人销售食品。区块链结合物联网也许能够解决这个问题。

随着供应链中的每个参与方都存储和检索信息,区块链可以加快查找食品来源的过程。以下是区块链可以添加的额外好处列表:

  • 消费者可以准确地了解食品产品的收获地点。

  • 由于对食物中毒的恐慌,人们倾向于扔掉干净的食物,这增加了食物浪费的数量。区块链可以准确定位有问题的食物,从而防止食物浪费。

  • 供应链中的每个步骤对所有人都是可见的。可以避免欺诈性食品进入市场。

  • 区块链可以作为证据证明某个有问题的食品是从特定生产者那里运出的。由于这个原因,生产者会小心并遵守安全操作规程,因为如果他们不这样做,就会被证据抓住。

  • 最后,每个食品都有一个与之相关的故事。这使用户能够了解食品的历史。

物联网技术,如传感器和射频识别标签,使食品产品沿着供应链传递时可以实时写入区块链的数据。

让我们看一个例子,了解在这种情况下区块链记录了什么,以及参与者是谁。参与者包括食物的原产地农场、打包加工厂、运输食物的货运公司、沃尔玛商店等等。记录在区块链上的数据包括农场原产地数据、批号、工厂和加工数据、过期日期、储存温度和运输细节。

加纳的土地登记

BenBen 是一支致力于为加纳建设创新产品以改善政府技术的研发工程师团队。他们利用区块链为加纳公民开发了数字土地登记解决方案。

在加纳,银行在发放贷款时不接受土地作为抵押品。这是因为在加纳,纸质登记系统在法庭上不可执行。这导致数百万人无法获得贷款。

BenBen 为金融机构提供了一流的栈顶土地登记和验证平台。该平台捕获交易并验证数据。BenBen 与金融机构合作更新当前登记册,启用智能交易,并为客户分发私钥,以实现各方之间自动化和可信赖的房地产交易。

迪拜的住房租赁

迪拜的住房租赁用例是一个区块链应用程序,让个人外籍人士在几分钟内在线租赁公寓或更新他们的住房租赁合同。在迪拜,如果一个人想租一间公寓,那么他们必须提供 KYC 文件,支票作为合同期限的保证,并创建 Ejari(迪拜政府合同,用于合法化迪拜房东和租户之间原本不愉快的关系)。在迪拜,大多数房地产公司只有在您想要长期居住(例如,至少一年)并确保您遵守合同的情况下才会租给您公寓,并要求您提供预期支票作为保证,因为在迪拜,支票被退回被视为犯罪行为。由于租赁公寓和续签租赁合同的过程对租户和房地产公司来说都是繁琐的过程,迪拜智能政府(Smart Dubai 的技术部门,致力于将迪拜在技术上进行转型的城市范围倡议)启动了一个使整个流程更轻松、更快速的使命,使用区块链。

此住房租赁应用程序是使用 Hyperledger Fabric 1.0 构建的,最初有七个实体参与了网络。迪拜智能政府(DSG,迪拜智能的技术部门,是一个旨在通过区块链使租赁合同的创建和续签更容易的全市倡议), 迪拜移民和外国人事务总局(DNRD),wasl,迪拜土地部门,迪拜电力和水务局(DEWA),阿联酋迪拜国民银行(NBD)和阿联酋伊斯兰银行(EI)是参与分享他们的区块链数据,以使租赁合同的创建和续签更加容易的实体。

早先,DSG 和阿联酋身份局EIDA)推出了 DubaiID,允许迪拜居民通过一个登录统一访问政府机构提供的所有电子服务,并通过互联网与之交互。在这个区块链用例中,租户必须使用 DubaiID 登录到房地产门户;在这种情况下,wasl 的租户必须有 DubaiID 才能登录。一旦登录,DSG 将阿联酋身份证号写入区块链,而 DNRD 则在区块链上为该租户共享签证和护照信息。然后,wasl 的门户将用户重定向到使用阿联酋 NBD 或 EI 银行账户提交数字支票。一旦提交了数字支票,就会通过区块链向 DLD 发出请求以续签或创建 Ejari。最后,一旦 Ejari 处理完成,DEWA 将被通知激活水电供应。所以基本上,第一个试点是为想要租赁或续签 wasl 公寓并拥有阿联酋 NBD 或 EI 银行账户的个人服务的。很快,更多的银行和房地产公司将被添加到网络中,以为迪拜更多的人提供此服务。在这个过程中,确保一条信息只能被相关方看到。

这个用例非常适合作为一个区块链用例,因为需要一个签名的不可变总账来存储 KYC、支票和 Ejaris,后者如果客户或任何实体试图进行欺诈,都可以得到证明。例如,当阿联酋 NBD 发放支票时,如果他们没有使用区块链,而是简单地进行点对点的 API 调用,那么 ENBD、租户和 wasl 之间关于数字支票的存在或当前状态存在故意和非故意的分歧的可能性就非常大。因此,如果发生任何争议,区块链可以成为最终的参考工具。

Ubin 项目

Ubin 项目是新加坡金融管理局(MAS)和 R3 合作的数字现金总账项目,参与方包括美国银行BOA美林证券瑞士信贷、星展银行、香港上海汇丰银行有限公司、摩根大通、三菱日联金融集团MUFG)、华侨银行、新加坡交易所SGX)和大华银行UOB),以及BCS 信息系统作为技术提供商。

Ubin 项目的目标是在分布式账本上构建 SGD(新加坡国家货币)的数字化形式,以为新加坡的金融生态系统带来许多好处。这些好处与任何其他加密货币的好处相同。

目前,该应用程序是使用 Quorum 构建的,但未来可能会转移到 Corda,因为 R3 是合作伙伴之一。

MAS 是新加坡的中央银行和金融监管机构。MAS 充当新加坡支付、清算和结算系统的结算代理、运营商和监管机构,着重于安全性和效率。

摘要

在本章中,我们了解了什么是 DApps,并对基于区块链的 DApps 进行了概述。我们了解了区块链是什么,它的好处是什么,并看到了我们可以使用的各种平台来构建基于区块链的 DApps。最后,我们看到了一些用例以及区块链如何为金融和非金融行业带来改变。在下一章中,我们将深入了解以太坊和 Quorum,并构建一个基本的示例 DApp。

第二章:使用 Quorum 构建区块链

在上一章中,我们了解了什么是 DApp、DLT 和区块链。我们还看到了一些流行的基于区块链的 DApp 的概述。目前,以太坊是继比特币之后最流行的公共 DApp。在本章中,我们将学习如何使用 Quorum 构建基于权限的区块链 DApp。我们将通过探索 Quorum 支持的所有不同共识协议、其权限和隐私功能以及最后能够快速部署 Quorum 网络的工具来深入了解 Quorum。

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

  • 以太坊用户账户

  • Merkle 树是什么以及在区块链中如何使用它?

  • 伊斯坦布尔拜占庭容错IBFT)和 Raft 如何工作?

  • Quorum 支持的各种机制,以实现隐私

  • 设置星座、Raft 和 IBFT 网络

  • 与 Quorum 相关的各种第三方工具或库

Quorum 概述

Quorum 是一个允许我们在其上部署 DApp 的权限分散平台。DApp 是使用一个或多个智能合约创建的。智能合约是按照程序编写的、没有任何停机、审查、欺诈或第三方接口可能性的程序。在 Quorum 中,智能合约可以用 Solidity、LLL 或 Serpent 编写。Solidity 是首选。一个智能合约可以有多个实例。每个实例由唯一地址标识,并且您可以在同一 Quorum 网络上部署多个 DApp。

在以太坊中,有一种名为以太的内部货币。要部署或执行智能合约,您需要向矿工支付以太,而 Quorum 是以太坊的一个分叉,这里也存在同样的情况。但在 Quorum 中,以太是无价值的,并且在创世块生成了固定数量的以太,之后不会再生成更多的以太。用户账户和智能合约都可以持有以太。在 Quorum 中,您需要一些以太来执行网络上的交易,但不会扣除以太,并且向另一个账户发送以太也不会扣除以太,因此可以说,在 Quorum 中,以太提供了一种跟踪账户所有者的方法,如果有任何可疑情况通过跟踪以太转账并提供一种方式使您需要从网络成员中获取一些以太才能进行交易的话,这就是,从一个允许的成员获取一些以太。

目前,Quorum 支持三种共识协议:QuorumChain、IBFT 和 Raft。在本书中,我们将只学习 Raft 和 IBFT,因为它们是最常用的。对于隐私,它支持两种机制:零知识安全层协议和私有合约。我们将学习私有合约,但不会涵盖 ZSL,因为它仍未达到生产就绪状态。

以太坊账户

要创建一个账户,我们只需要一个非对称密钥对。有各种算法,例如Rivest–Shamir–AdlemanRSA)和椭圆曲线密码学ECC)用于生成非对称密钥对。以太坊使用 ECC。ECC 有各种曲线。这些曲线具有不同的速度和安全性。以太坊使用secp256k1曲线。深入了解 ECC 及其曲线需要数学知识,但深入理解它并不是构建使用以太坊的 DApps 所必需的。

以太坊使用 256 位加密。以太坊的私钥和公钥是一个 256 位的数字。由于处理器无法表示如此大的数字,因此它总是被编码为长度为 64 的十六进制字符串。

每个账户都由一个地址表示。一旦我们有了生成地址所需的密钥,这里是生成地址的步骤,以及从公钥生成地址的步骤:

  1. 首先,生成公钥的Keccak-256哈希值。它将给你一个 256 位的数字。

  2. 丢弃前 90 位和 12 字节。现在,您应该有 160 位的二进制数据(20 字节)。

  3. 现在,将地址编码为十六进制字符串。因此,最后,您将得到一个 40 个字符的字节字符串,这就是您的账户地址。

现在,任何人都可以向这个地址发送以太币,然后您可以签名并从这个地址发送交易。

以太坊交易是什么?

交易是一个数据包,用于将以太币从一个账户转移到另一个账户或合约,调用合约的方法,或部署一个新的合约。交易使用椭圆曲线数字签名算法ECDSA),这是一种基于 ECC 的数字签名算法。一笔交易包含一个标识发送者并证明其意图的签名,要转移的以太币金额,交易执行允许的最大计算步骤数(称为燃气限额),以及发送交易的人愿意支付的每个计算步骤的成本(称为燃气价格)。用燃气使用量乘以燃气价格得到的乘积称为交易费用

在受权限控制的网络中,以太币是无价值的。在 Quorum 网络中,以太币在创世块中提供,并且不是在运行时动态生成的。您需要在创世块中提供以太币。您需要提供燃气以防止攻击,例如无限循环。交易被挖掘时,以太币不会从账户中扣除。

如果交易的意图是调用合约的方法,它还包含输入数据,或者如果它的意图是部署一个合约,那么它可以包含初始化代码。要发送以太币或执行合约方法,您需要向网络广播一个交易。发送者需要用其私钥签名交易。

如果我们确信一笔交易将始终出现在区块链的同一位置,我们就说该交易已确认。在以太坊的工作证明中,建议在最新区块的下方等待该交易出现 15 个区块(即等待 15 个确认)再假定该交易已确认,因为存在分叉的可能性,交易可能从区块链中消失。但是,在 Quorum 的 Raft 或 IBFT 中,一旦交易出现在其中一个区块中,我们就可以说它已确认,因为没有分叉的可能性。

什么是 Merkle 树?

在我们深入了解区块链中区块的 Merkle 根之前,让我们先了解区块链的结构。一个区块由两部分组成;第一部分是区块头,第二部分是该区块的交易集。区块头包含诸如上一个区块哈希(实际上是上一个区块头的哈希)、时间戳、Merkle 根以及与达成共识相关的信息。

在同步时,当一个节点下载一个区块时,该节点下载区块头和区块的交易。那么,接收节点如何知道这些交易实际上是该区块的一部分,并且按正确的顺序排列的呢?每个区块都由唯一的哈希标识,但是区块哈希不是区块头的一部分,并且是每个节点在下载区块后独立计算的唯一哈希;因此我们不能使用区块哈希的概念。相反,我们可以依赖类似交易哈希的东西;一个存储在区块头中的哈希,通过组合所有交易并对其进行哈希计算得到。这个想法将完美地发挥作用,并且我们可以检测到是否有任何交易丢失或额外交易包含在内,或者交易是否按正确的顺序。

嗯,Merkle 根是交易哈希方法的一种替代方法,但提供了另一个主要优势:它允许网络拥有轻节点。当然,我们可以实现没有 Merkle 根的区块链,但如果网络需要轻节点,则必须使用 Merkle 根。轻节点是仅下载区块头而不下载交易的节点,但仍应能够为客户端提供所有 API。例如:智能手机无法拥有完整的区块链,因为它可能非常庞大;因此,我们可以在智能手机中安装轻客户端。

让我们首先了解二进制 Merkle 树在区块链中是什么。哈希树或 Merkle 树是一种树,其中每个叶节点是一个交易的哈希,每个非叶节点是其子节点的哈希的哈希。哈希树允许高效和安全地验证哪些交易是区块的一部分。每个区块都形成自己的 Merkle 树。当每个父节点都有两个子节点时,Merkle 树被称为二进制 Merkle 树。二进制 Merkle 树是区块链中使用的树。以下是二进制 Merkle 树的示例:

在上面的图表中,首先计算每个交易的单独哈希。然后,它们被分成两组。然后,对于每一对,计算两个哈希的哈希。这个过程将持续进行,直到我们有一个称为默克尔根的单一哈希。如果交易数量为奇数,最后一个交易会被复制,以使交易总数为偶数。

现在,在下载完整区块、区块头和区块的交易时,节点可以通过形成二叉默克尔树并检查生成的默克尔根是否与包含在区块头中的默克尔根相同,来验证交易集是否正确。当然,这也可以在没有默克尔树的情况下完成,正如前面讨论过的。

轻节点可以利用默克尔树为客户端提供服务。例如,轻节点可以向完整节点发出请求,询问特定交易是否已经在某个区块中提交,完整节点会回复区块编号和默克尔证明,以证明交易是否已经在某个区块中提交。然而,轻节点不能仅仅相信完整节点提供的区块编号,因此完整节点还需提供默克尔证明。为了理解什么是默克尔证明,让我们来看看前面的图表以及轻节点询问完整节点 TxD 是否已提交的情况。现在,完整节点返回区块编号以及一个子树,其中包括 H[ABCD]H[AB]H[CD]H[C]H[D]。这个子树就是默克尔证明。现在,轻客户端可以拿到这个默克尔证明并验证它。验证将包括检查默克尔证明是否构造正确以及默克尔证明的默克尔根是否与完整节点声称交易所在的区块头中的默克尔根相同。你可能会想,如果一个完整节点声称交易已提交,但实际上尚未提交,该怎么办呢?在这种情况下,解决这个问题的唯一方法是请求多个完整节点,而且它们都不太可能撒谎。没有默克尔树,这个功能是无法实现的。

以太坊区块链更加复杂。现在,假设以太坊轻节点想要知道某个账户的余额、从智能合约中读取数据、找到交易的燃气估算值等等,那么通过这种简单的交易二叉默克尔树将无法提供这种功能。因此,在以太坊中,每个区块头不仅包含一个默克尔树,而是包含三个默克尔树,用于三种类型的对象:

  • 交易

  • 交易收据(基本上是显示每个交易效果的数据片段)

  • 状态

现在我们有了三棵树,让我们来看一个轻节点向完整节点提出的高级查询示例。查询是假装在这个合约上运行这个交易。交易收据和新状态会是什么? 这由状态树处理,但计算方式更为复杂。在这里,我们需要构造一个Merkle 状态转换证明。基本上,它是一种证明,宣称:如果在具有根S的状态上运行交易T,则结果将是具有根S'的状态,其中包括交易收据R。为了计算状态交易证明,完整节点在本地创建一个虚假块,将状态设置为S,并假装成为一个轻节点,同时应用交易。也就是说,如果应用交易的过程需要轻节点确定账户余额,则轻节点进行余额查询。如果轻节点需要检查特定合约的存储中的特定项目,则轻节点进行该查询,依此类推。完整节点正确响应自己的所有查询,但跟踪发送回的所有数据。然后完整节点将来自所有这些请求的组合数据作为证明发送给轻节点。然后轻客户端执行完全相同的过程,但使用提供的证明作为其数据库,而不是向完整节点发出请求,如果其结果与完整节点声称的相同,则轻客户端接受输出为完整节点所声称的输出。

对于状态,以太坊使用Merkle Patricia 树而不是二叉树。对于状态树,情况更加复杂。以太坊中的状态基本上由键值映射组成,其中键是地址,值是每个账户的账户余额、nonce、代码和存储(其中存储本身是一棵树)。要了解 Merkle Patricia 树的工作原理,请访问easythereentropy.wordpress.com/2014/06/04/understanding-the-ethereum-trie/.

在企业区块链中,不使用轻客户端,因为节点代表一个企业,而企业有基础设施来运行完整节点。

区块链中的分叉是什么?

当节点之间关于区块链的有效性存在冲突时,即网络中存在多个区块链时,就会发生分叉。分叉有三种类型:常规、软分叉和硬分叉。

当同时存在两个或更多个相同高度的区块时,就会发生常规分叉。这是一种暂时的冲突,会自动解决。节点通过选择最准确的区块链来解决这个问题。例如,在工作量证明中,如果两个矿工同时挖出一个区块,那么就会创建一个常规分叉。这是通过选择具有最高难度的区块链来解决的,因为它被认为是最准确的一个。

相比之下,软分叉是指对区块链协议的任何更改都是向后兼容的。比如,新规则可能只允许 1 MB 区块,而不是 2 MB 区块。非升级的节点仍然会将新的交易视为有效(在本例中,1 MB 小于 2 MB)。然而,如果非升级的节点继续创建区块,那么它们创建的区块将被升级的节点拒绝。因此,如果网络中的少数节点升级了,则它们形成的链将变得不太准确,并被非升级节点创建的区块链覆盖。软分叉在网络中的大多数节点升级其节点软件时解决。

硬分叉是一种软件升级,引入了一个与旧软件不兼容的新规则到网络中。你可以把硬分叉看作是规则的扩展。例如,允许区块大小为 2 MB 而不是 1 MB 的新规则将需要进行硬分叉。继续运行旧版本软件的节点将会将新交易视为无效。因此,只有当网络中的所有节点都升级其节点软件时,分叉才能解决。在那之前,网络中将会有两个不同的区块链。

你一定听说过比特币和以太坊的分叉。例如,比特币现金和以太经典是硬分叉的形成。网络中的许多矿工和节点不同意新协议,并选择运行旧软件,从而分裂出网络并形成了一个不同的网络。

Raft 共识

让我们看看 Raft 共识协议的工作原理,以一个足以让我们舒适地构建 DApps 的水平。我们不会深入研究 Raft,因为这并不必要。

Raft 用于半可信网络,并且有一种希望获得更快的区块时间(以毫秒为单位而不是秒)和单次确认(没有定期的分叉)的愿望。

网络中的每个节点都会保留网络中所有其他节点的列表,无论它们是否正在运行。Raft 集群中的服务器可以是领导者或追随者,并且在选举时可能是候选者,这种情况发生在领导者不可用时。每次只能有一个领导者。领导者负责创建和发送区块给追随者。它通过定期发送心跳消息来告知追随者自己的存在。每个追随者都有一个超时(通常在 150 和 300 毫秒之间),称为选举超时,在此期间它期望来自领导者的心跳。每个节点都使用在 120-300 ms 范围内的随机选举超时。收到心跳后,选举超时会被重置。如果没有收到心跳,追随者会将其状态更改为候选者,并开始领导者选举,以选举网络中的新领导者。当候选者启动领导者选举时,它基本上将自己提出为新领导者,并且如果超过 50%的节点投票支持它,它就成为领导者。如果在一定的超时内没有选举出领导者,则会启动新的领导者选举过程。深入理解领导者选举过程并非必要。

Raft 的设计是这样的,一个 Raft 网络需要超过 50%的节点可用,才能将新的交易提交到区块链;如果集群有2 * F + 1个节点,则可以容忍F个故障并仍然正常运行。如果超过F个节点失败,则应用程序将失败,并且一旦集群再次有超过 F 个节点正常工作,它将再次正确地恢复工作。即使领导者选举也会在网络中超过 50%的节点不可用时失败。

每个节点的每笔交易都会发送到网络中的每个其他节点。领导者负责创建和广播区块。当领导者创建一个区块时,它首先将区块发送给所有的追随者,一旦超过 50%的追随者接收到了该区块,领导者将把该区块提交到其区块链中,然后向追随者发送一个提交消息,以便追随者也将该区块提交到其区块链中。在追随者不可用的情况下,领导者会无限期地重试请求,直到所有的追随者最终都提交了该区块。这个过程确保了一旦一个区块提交到区块链上,就无法撤销。即使领导者选举过程也确保了谁被选为领导者,其区块链是最新的。

在 Quorum 中,默认的区块时间是 50 毫秒,您可以根据需要进行更改。因此,每 50 毫秒会创建一个区块,但请记住,如果没有交易,那么就不会创建区块;在 Raft 中不会创建空块。领导者可以在先前的区块提交之前创建新的区块并将其发送给跟随者,区块创建是异步的。但是当然,它们是按顺序提交的。当节点启动时,它只会从领导者那里获取丢失的区块,而不会从网络中的其他节点获取。

对于 Raft 集群的正常运行,非常重要的一点是,服务器发送心跳请求到集群中的每个服务器并接收响应的平均时间小于选举超时时间。此外,领导者没有办法删除或修改已提交的区块;领导者只能将新的区块追加到区块链中。

伊斯坦布尔拜占庭容错

让我们看看 IBFT 共识协议是如何工作的,这将使我们足够放心去构建 DApps。我们不会深入研究 IBFT,因为这并不是必要的。

IBFT 是一种权威证明协议。在 IBFT 中,有两种类型的节点:验证者节点(当它们与物理实体连接时称为权威节点)和常规节点。权威节点是创建区块的节点。IBFT 用于需要 BFT 的网络,几秒钟的区块时间足够好,并且我们需要单一确认(没有常规分叉)。

系统最多能容忍 F 个拜占庭或崩溃节点,在一个 N 个验证节点网络中,即 F = (N-1)/3。IBFT 中的默认区块时间为一到十秒,Quorum 允许您自定义此时间。

在 IBFT 中,一个轮次涉及创建并提交一个新的区块到区块链中。在 (2F + 1) 个验证者的区块链中提交了一个新的区块后,就会开始一个新的轮次。在每个区块创建轮次之前,验证者将从中选择一个作为提议者。提议者是负责创建区块的验证者。为了将区块提交到区块链上,必须至少有 (2F + 1) 个验证者签名。因此,在每一轮中,提议者和其他验证者之间需要发送和接收各种消息的过程以同意新的区块。

夸姆支持两种算法来选择提议者:轮询和粘性提议者。默认情况下使用轮询,而在轮询算法中,提议者按轮询方式选择。但是,在粘性提议者算法中,单个验证者成为所有轮次的提议者,如果提议者崩溃,则选择下一个验证者作为新的提议者,其再次成为所有轮次的唯一提议者;提议者保持不变,直到失败。无论是轮询还是粘性提议者算法,如果提议者在1-10秒的时间内未能提交区块,则会启动新一轮,下一个验证者成为新一轮的提议者。

如果网络设法拥有多于F个故障节点,则这些故障节点可以通过拒绝签署区块来阻止新区块的创建。当网络中的崩溃节点重新上线时,它可以从网络中的任何节点获取丢失的区块。超过F个故障节点无法重新编写区块。

验证者列表存储在创世区块的头部,并且头部的extraData字段包含验证者列表。对于第一轮,选择第一个验证者。头部还包含与 IBFT 相关的各种其他字段和详细信息,以帮助网络达成共识。

验证者可以添加或删除验证者。即使将新验证者添加或删除到网络中也需要2F + 1个验证者同意。验证者同意或不同意添加或删除验证者的过程是手动进行的。它不能是一个自动过程,因为验证者可以开始添加多个自己的验证节点并危害网络。因此,手动过程确保其他验证者了解新验证者是谁,并决定是否允许它。

你可以在github.com/ethereum/EIPs/issues/650.深入了解 IBFT 的工作原理。

私有合约和星座

私有合约是夸姆提供的一个开箱即用的功能,用于实现数据隐私。私有合约用于在两个或多个节点之间私下共享信息,而其他节点无法看到。

让我们看看 Quorum 中的私有合约是什么。使用私有交易部署的合约称为私有合约。私有交易基本上是一种其有效负载(合约部署的合约代码或调用函数的函数参数,交易的数据部分)在区块链之外点对点共享的交易,在发送交易时选择的一组节点之间共享有效负载,并且有效负载的哈希在区块链中被记录,用有效负载的哈希替换实际有效负载。现在,网络中的节点检查它们是否有内容哈希为区块链中存在的有效负载的哈希,并且如果是,则执行原始有效负载。Quorum 形成同一区块链的两个不同状态:公共状态和私有状态。私有交易形成私有状态,而公共交易形成公共状态。这些状态之间不能互相交互。但是,私有-私有合约可以相互交互。

Quorum 使用 constellation 来发送和接收私有交易的实际交易有效负载。Constellation 是由 J.P. Morgan 构建的独立软件。Constellation 形成一个节点网络,每个节点都会公布一个它们是接收方的公钥列表。每个节点暴露了一个 API,允许用户将有效负载发送到一个或多个公钥。在传输到接收节点之前,有效负载将被加密为公钥。它通过 IPC 公开了应用程序连接到其 constellation 节点并发送或接收数据的 API。在高层次上,如果您连接到 constellation 网络,则只需提及接收方的公钥,数据就会被加密并发送到与公钥映射的 IP 地址。在发送私有交易时,仅在有效负载成功发送到所有列出的 constellation 节点后,才将公钥列表和交易广播到区块链网络。如果任何列出的 constellation 节点宕机,则交易失败,并且永远不会广播到区块链网络。

所以,在启动您的 Quorum 节点之前,您需要启动您的 constellation 节点,并在启动 Quorum 节点之前提供 constellation 的 IPC 路径给 Quorum 节点。然后,您的 Quorum 节点使用 constellation 来发送或接收私有交易。

私有交易并不是在 Quorum 中实现隐私的最终解决方案。它们有各种缺点。以下是其中一些:

  • 一旦您向一组节点发送了私有交易,就无法将新节点添加到该列表中。例如,如果您部署了用于银行间转账的私有合约。假设最初中央银行不是网络的一部分,如果后来他们决定加入,那么他们将无法监视交易,因为我们无法使私有合约对他们可见,也不能使之前的银行转账对他们可见。虽然他们可以看到新的私有交易,但由于他们没有新的私有交易,他们无法执行交易,因此将无法查看输出。

  • 无法检查指向私有合约的私有交易是否具有与部署私有合约时使用的完全相同的公钥列表。这可能导致双重花费攻击;换句话说,您可以将相同的资产转移两次。例如,在部署合约时,您提到了三个节点 ABC。现在,当 A 转移资产时,它可能会从私有交易中排除 C,然后稍后通过创建新的私有交易将相同的资产转移到 CC 无法验证资产的新所有者是 B。因此,私有交易不用于转移数字资产,但私有交易可用于所有其他形式的数据表示。

  • 您需要为星座节点构建自己的备份机制。因此,如果您的星座节点崩溃,则不会自动从星座网络中获取有效负载。

安装 Quorum 和星座

现在我们对 Quorum 的共识协议、以太坊账户、交易和私有合约非常有信心。是时候构建一个 Quorum 网络了。在此之前,我们需要学习如何安装 Quorum 和星座。请记住,星座是可选的,仅在需要私有合约时才应将其集成到 Quorum 网络中。

安装 Quorum 和星座的最佳方法是构建源代码。在本书中,我们将仅集中在 Ubuntu 和 macOS 上的步骤。您可以在github.com/jpmorganchase/quorum找到 Quorum 的源代码,而星座源代码可以在github.com/jpmorganchase/constellation找到。

以下是从源代码构建 Quorum 的三个基本命令:

git clone https://github.com/jpmorganchase/quorum.git
cd quorum
make all

现在,进入 build/bin/ 目录,您会找到 geth 可执行文件,这是运行 Quorum 节点的节点软件。此外,您还会找到另一个名为 bootnode 的可执行文件,我们将仅使用它来生成 enode ID。稍后我们将看到什么是 enode ID。

要安装星座(constellation),您需要几个先决条件。在 Ubuntu 中,运行以下命令安装先决条件:

apt-get install libdb-dev libleveldb-dev libsodium-dev zlib1g-dev libtinfo-dev
curl -sSL https://get.haskellstack.org/ | sh
stack setup  

而在 macOS 中,运行以下命令安装先决条件:


brew install berkeley-db leveldb libsodium
brew install haskell-stack
stack setup

现在,要安装星座,运行以下命令:

git clone https://github.com/jpmorganchase/constellation.git
cd constellation
stack install

现在,在运行上述命令并成功执行后,你将收到一条消息,指明了constellation-node可执行文件的路径。将可执行文件从那个路径移动到一个方便你找到的地方。

构建你的第一个 Raft 网络

现在,我们已经成功安装了 Quorum 和星座,现在是时候设置我们的第一个 Quorum 网络了。在设置网络之前,你需要决定是否要使用 Raft 还是 IBFT,然后相应地进行计划和设置。我们将学习如何设置这两种类型的网络。我们还将设置一个星座网络。

现在,让我们使用星座构建一个 Raft 网络。一旦网络运行起来,我们还将看到如何添加和删除新节点。我们将构建一个四个节点的网络。

创建一个名为raft的目录。然后,在其中放置gethconstellation-node的二进制文件。你可以使用gethconstellation-node--help选项来查找各种子命令和可用选项。

设置星座网络

现在,让我们首先创建四个星座节点。为了开发目的,我们将在同一台机器上运行所有四个节点。对于每个星座节点,我们必须生成一个单独的非对称密钥对。在raft目录中运行以下命令来创建密钥对:

./constellation-node --generatekeys=node1
./constellation-node --generatekeys=node2
./constellation-node --generatekeys=node3
./constellation-node --generatekeys=node4

在这里,我们为每个星座节点生成了一个公钥。但是,你可以为每个星座节点有多个公钥。在运行上述命令时,它将要求你输入一个密码来加密密钥,但是你可以通过按下Enter键来跳过这一步。如果你想在运行星座节点时加密,则必须提供解密密码。为了让事情简单,我们将不设置密码。

在启动星座节点时,你需要传递各种必需和可选变量,例如广告给其他节点的 URL(它们可以访问的),本地监听的端口,存储负载、公钥、私钥、TLS 设置等的目录。你可以将这些变量作为命令的选项传递给星座节点,或者以配置文件的形式传递。让我们为每个星座节点创建一个配置文件,为星座节点提供这些设置以启动。以下是星座节点的配置文件:

以下是constellation1.conf的代码:

url = "http://127.0.0.1:9001/"
port = 9001
storage = "dir:./cnode_data/cnode1/"
socket = "./cnode_data/cnode1/constellation_node1.ipc"
othernodes = ["http://127.0.0.1:9002/", "http://127.0.0.1:9003/", "http://127.0.0.1:9004/"]
publickeys = ["./cnode1.pub"]
privatekeys = ["./cnode1.key"]
tls = "off"

以下是constellation2.conf的代码:

url = "http://127.0.0.1:9002/"
port = 9002
storage = "dir:./cnode_data/cnode2/"
socket = "./cnode_data/cnode1/constellation_node2.ipc"
othernodes = ["http://127.0.0.1:9001/"]
publickeys = ["./cnode2.pub"]
privatekeys = ["./cnode2.key"]
tls = "off"

以下是constellation3.conf的代码:

url = "http://127.0.0.1:9003/"
port = 9003
storage = "dir:./cnode_data/cnode3/"
socket = "./cnode_data/cnode1/constellation_node3.ipc"
othernodes = ["http://127.0.0.1:9001/"]
publickeys = ["./cnode3.pub"]
privatekeys = ["./cnode3.key"]
tls = "off"

以下是constellation4.conf的代码:

url = "http://127.0.0.1:9004/"
port = 9004
storage = "dir:./cnode_data/cnode4/"
socket = "./cnode_data/cnode1/constellation_node4.ipc"
othernodes = ["http://127.0.0.1:9001/"]
publickeys = ["./cnode4.pub"]
privatekeys = ["./cnode4.key"]
tls = "off"

这里,变量名揭示了变量的含义。需要注意的一点是,在最后三个节点中我们并没有提供其他三个节点的 URL,因为 constellation 具有内置的自动发现协议来发现网络中的节点。所以,第一个节点指向最后三个节点,而最后三个节点与第一个节点有连接,但最终所有节点都能找到彼此。

现在,在不同的 shell 窗口中运行以下命令来启动 constellation 节点:

./constellation-node constellation1.conf
./constellation-node constellation2.conf
./constellation-node constellation3.conf
./constellation-node constellation4.conf

生成 enode

在 Raft 中,在设置网络之前,你必须确定网络中将有多少个节点,然后为每个节点生成并 enode ID。随后,你创建一个列出所有节点的 enode URL 的 static-nodes.json 文件,并将此文件提供给网络中的每个节点。一旦配置好网络,向网络中添加节点涉及不同的流程。

在继续之前,你需要了解以太坊中的 enode 是什么。Enode 是一种以 URI 形式描述以太坊节点的方式。网络中的每个节点都有一个不同的 enode。Enode 包含一个名为节点 ID的 512 位公钥,用于验证网络上特定节点的通信。Enode 还包含节点 ID 的 IP 地址和端口号。与节点 ID 相关联的私钥称为节点密钥

我们将建立一个由三个节点组成的网络,然后动态添加第四个节点。使用以下三条命令生成所有四个节点的节点密钥:

./bootnode -genkey enode_id_1
./bootnode -genkey enode_id_2
./bootnode -genkey enode_id_3
./bootnode -genkey enode_id_4

上述命令将创建私钥。现在,要查找节点 ID,需要运行下面的命令:

./bootnode -nodekey enode_id_1
./bootnode -nodekey enode_id_2
./bootnode -nodekey enode_id_3
./bootnode -nodekey enode_id_4

上述命令将不会创建任何新文件;相反,它们将仅打印带有相应私钥相关联的实际节点 ID 的示例节点 URL。例如:enode://[nodeID]@[IP]:[port]

现在,创建一个名为 static-nodes.json 的文件,并添加以下代码。确保将节点 ID 替换为你生成的节点 ID:

[
 "enode://480cd6ab5c7910af0e413e17135d494d9a6b74c9d67692b0611e4eefea1cd082adbdaa4c22467c583fb881e30fda415f0f84cfea7ddd7df45e1e7499ad3c680c@127.0.0.1:23000?raftport=21000",
 "enode://60998b26d4a1ecbb29eff66c428c73f02e2b8a2936c4bbb46581ef59b2678b7023d300a31b899a7d82cae3cbb6f394de80d07820e0689b505c99920803d5029a@127.0.0.1:23001?raftport=21001",
 "enode://e03f30b25c1739d203dd85e2dcc0ad79d53fa776034074134ec2bf128e609a0521f35ed341edd12e43e436f08620ea68d39c05f63281772b4cce15b21d27941e@127.0.0.1:23002?raftport=21002"
]

这里,2300x 端口用于以太坊协议通信,2100x 端口用于 Raft 协议通信。

在以太坊中,static-nodes.json 被用来列出一些你总是想要连接的节点的 enode。并且,使用这些节点,你的节点可以发现网络中的其他节点。但在 Quorum 的 Raft 中,这个文件必须包含网络中所有节点的 enode,因为在 Raft 中,这个文件用于达成共识,不同于以太坊中的用途是节点发现。

创建一个账户

现在,我们需要生成一个以太坊账户。现在进行这个操作是因为在创建创世块时,我们必须为网络提供以太币。因此,我们将为此生成的账户提供以太币。以下是创建以太坊账户的命令:

./geth --datadir ./accounts account new

在运行此命令时,它将要求输入密码以加密帐户。 您可以按两次 Enter 键跳过。 这将使空字符串成为解密帐户的密码。 在这里,--datadir 选项用于指定在哪里存储密钥。 基本上,在 accounts/keystore 目录中,您将找到一个格式为 UTC--DATE-TIME--ADDRESS 的文件。 将此文件重命名为 key1。 此文件存储帐户的私钥和地址。 打开文件并复制地址,因为您在创建创世块时将需要它。

创建创世块

现在,最后一步是创建创世块。 创世块始终在网络中硬编码。 以下是创世块的内容。 创建一个 genesis.json 文件并将以下代码放入其中:

{
     "alloc": {
         "0x65d8c00633404140986e5e23aa9de8ea689c1d05": {
             "balance": "1000000000000000000000000000"
          }
     },
     "coinbase": "0x0000000000000000000000000000000000000000",
     "config": {
         "homesteadBlock": 0
     },
     "difficulty": "0x0",
     "extraData": "0x",
     "gasLimit": "0x7FFFFFFFFFFFFFFF",
     "mixhash": "0x00000000000000000000000000000000000000
       647572616c65787365646c6578",
     "nonce": "0x0",
     "parentHash": "0x00000000000000000000000000000000
       00000000000000000000000000000000",
     "timestamp": "0x00"
}

在这里,请确保用您的帐户地址 0x65d8c00633404140986e5e23aa9de8ea689c1d05 替换帐户地址。 在这里,我们向 0x65d8c00633404140986e5e23aa9de8ea689c1d05 帐户提供了以太币。

如果您想在 Quorum 网络中摆脱以太币,可以在启动 geth 时使用 --gasPrice 0 选项。 因此,您将不需要在创世块中提供以太币。 但是,以太币具有可追溯性的优势。

启动节点

现在,在我们启动节点之前,我们需要初始化它们并为每个节点创建数据目录;将 static-nodes.json 文件复制到每个节点的数据目录中,将帐户密钥复制到数据目录中,并使用创世块引导区块链。

以太坊节点的数据目录结构包括 gethkeystore 目录,还有一个 static-nodes.json 文件。 keystore 目录包含帐户文件,而 geth 目录包含与以太坊相关的所有其他数据,例如区块链交易、状态和 enode 密钥。

以下是对所有节点执行所有初始化操作的命令:

#Configuring Node 1
#'keystore' dir stores acccounts and 'geth' dir stores all other data
mkdir -p qdata/node1/{keystore,geth} 
cp static-nodes.json qdata/node1
cp accounts/keystore/key1 qdata/node1/keystore
cp enode_id_1 qdata/node1/geth/nodekey
./geth --datadir qdata/node1 init genesis.json #bootstrap the blockchain

#Configuring Node 2
mkdir -p qdata/node2/geth
cp static-nodes.json qdata/node2
cp enode_id_2 qdata/node2/geth/nodekey
./geth --datadir qdata/node2 init genesis.json

#Configuring Node 3
mkdir -p qdata/node3/geth
cp static-nodes.json qdata/node3
cp enode_id_3 qdata/node3/geth/nodekey
./geth --datadir qdata/node3 init genesis.json

上述命令是不言自明的。 现在,运行以下命令以启动 Quorum 节点。 在新的 shell 窗口中运行每个命令:

#Starting node 1
PRIVATE_CONFIG=constellation1.conf ./geth --datadir qdata/node1 --port 23000 --raftport 21000 --raft --ipcpath "./geth.ipc"

#Starting node 2
PRIVATE_CONFIG=constellation2.conf ./geth --datadir qdata/node2 --port 23001 --raftport 21001 --raft --ipcpath "./geth.ipc"

#Starting node 3
PRIVATE_CONFIG=constellation3.conf ./geth --datadir qdata/node3 --port 23002 --raftport 21002 --raft --ipcpath "./geth.ipc"

以下是我们提供的不同选项的含义:

  • PRIVATE_CONFIG:此变量用于使 geth 知道需要将私有负载发送到的 constellation 节点。 它指向 constellation 节点的配置文件。

  • --datadir:用于存储状态、交易、帐户等的数据目录。

  • --raft:用于指定我们要运行 Raft 共识。

  • --port:用于绑定以太坊传输的端口。

  • --raft-port:用于绑定 Raft 传输的端口。

  • --ipcpath:IPC 套接字和管道的文件名。 IPC 默认已启用。

geth为客户端提供了用于与其通信的 JSON-RPC API。geth使用 HTTP、WS 和 IPC 提供 JSON-RPC API。JSON-RPC 提供的 API 分为各种类别。geth还提供了一个交互式的 JavaScript 控制台,以便使用 JavaScript API 对其进行编程交互。交互式控制台使用 IPC 上的 JSON-RPC 与geth进行通信。我们稍后会详细了解这个。

现在,要打开node1的交互式控制台,请使用以下命令:

./geth attach ipc:./qdata/node1/geth.ipc

现在,我们已经完成了创建我们的第一个 Raft 网络。

动态添加或移除节点

现在,让我们动态添加第四个节点。 任何节点都可以向网络添加第四个节点。 让我们从node1添加它。 第一步是对node4进行初始化操作。 为此运行以下命令:

#Configuring Node 4
mkdir -p qdata/node4/geth
cp enode_id_4 qdata/node4/geth/nodekey
./geth --datadir qdata/node4 init genesis.json

请注意,这里我们没有复制static-nodes.json文件,因为我们是动态添加它。 现在,从第四个节点的交互式控制台中,运行以下行代码将第四个对等体添加到网络中:

raft.addPeer("enode://27d3105b2c1173792786ab40e466fda80edf9582cd7fa1a867123dab9e2f170be0b7e16d4065cbe81637759555603cc0619fcdf0fc7296d506b9c26c26f3ae0c@127.0.0.1:23003?raftport=21003") 

在这里,请用您生成的节点 ID 替换节点 ID。 当您运行以下命令时,将获得一个数字作为返回值。 这个数字很重要,被称为节点的 Raft ID。 Raft 共识为每个节点分配一个唯一 ID。 静态nodes.json文件中的第一个节点被赋予 Raft ID 1,下一个节点被赋予 Raft ID 2,依此类推。第四个节点将具有 Raft ID 4。 在启动第四个节点时,您将需要这个数字。 现在,使用以下命令启动第四个节点:

PRIVATE_CONFIG=constellation4.conf ./geth --datadir qdata/node4 --port 23003 --raftport 21003 --raft --ipcpath "./geth.ipc" --raftjoinexisting 4

在前面的命令中,一切看起来都很相似,除了一个新选项,--raftjoinexisting。 在启动动态添加的节点时,我们需要指定此选项并赋予它节点的 Raft ID。 当使用raft.addPeer添加节点时,这个 Raft ID 将出现。

现在,让我们从网络中移除一个节点。 让我们从static-nodes.json文件中删除第三个节点。 这个节点的raft ID 将是3。 在节点 1 的交互式控制台中,运行以下代码从网络中移除第三个节点:

raft.removePeer(3)

现在,第三个对等体将从网络中移除。 您现在可以使用admin.peers API 来检查连接到此节点的节点总数列表。 列表中应该有两个节点,网络中共有三个节点。

如果一个节点在添加或移除新节点到网络时处于宕机状态,那么宕机的节点将在其恢复运行后知道网络的更改。

搭建你的第一个 IBFT 网络

我们将构建一个六个节点的网络。 前四个将是验证者,另外两个将是非验证者。 在这个网络中,我们将不添加星座。 如果您想添加一个,指令与之前相同。

在 IBFT 中,每个验证者都是通过从其节点密钥派生的以太坊账户唯一标识的。类似于 Raft,在设置网络之前,我们在 IBFT 中必须决定网络中的验证者总数,然后为每个验证者生成一个 enode。然后,我们创建一个列出所有验证节点 enode 的 static-nodes.json 文件,并将此文件提供给网络中的每个验证者。之后,从节点 ID 派生以太坊地址。最后,我们构造 extraData 字段并创建 genesis 文件。

对于 IBFT,创建 static-nodes.json 文件并非必需。你也可以使用 admin.addPeer(url) API 连接节点。

安装 IBFT 工具

IBFT 软件包含配置 IBFT 网络、生成 enode、从节点密钥生成地址、创建创世块等工具。为 IBFT 创建创世块并不像为 Raft 创建那样简单,因为创世块中需要包含一个编码的 extraData 字段,其中列出了验证者列表。

以下是安装 IBFT 工具的步骤:

cd ~
mkdir -p go/src/github.com/getamis
cd go/src/github.com/getamis
git clone https://github.com/getamis/istanbul-tools.git
export GOPATH=~/go
go get github.com/getamis/istanbul-tools/cmd/istanbul
cd ~/go/bin

现在,在 ~/go/bin 目录中,你会找到一个名为 istanbul 的可执行文件。这是用于创建创世块的工具。创建一个名为 ibft 的目录,并将可执行文件移动到那里。

创建一个创世块

IBFT 工具可以自动创建创世块。同时,它还会生成节点密钥、从节点密钥生成的地址和 static-nodes.json 文件。

运行以下命令以生成所有这些内容:

./istanbul setup --num 4 --nodes --verbose

现在,你会得到类似的输出:

validators
{
 "Address": "0x05a6245732c2350ba2ed64e840394c2239f8ad1f",
 "Nodekey": "eae5093e524bf14ba6e95c13591d6a785be9ea486b9e8e9c1281314f75a3d4f9",
 "NodeInfo":     "enode://bd1049d796f1b71bef17d428ce8db5f22e478ecbeb9513c57e90d93ca1e9ec107f4f4b43585556ca8bb3ab630f1f6543d0d4147f5d890e1fde301b2af1fd7a08@0.0.0.0:30303?discport=0"
}
{
 "Address": "0x97a80dc7a7e27f41ae006fa1253f1f105f77335c",
 "Nodekey": "decc1787fda1f4079511bcff92e83f868755c8e06636303c42cfb3cce554919e",
 "NodeInfo":     "enode://6344e12a9b3f4fd5c154ee13ebe5351a5460a44302fd493a5e742adf8a294b6dc112fab1fa8ff19dde0027373c96c51ab6254153877c9fadabfc057624e522f0@0.0.0.0:30303?discport=0"
}
{
 "Address": "0xf69faf33e8690e82b0043e9131e09bbbc394cbed",
 "Nodekey": "7e1a7660f4ec525096ebea34a7a3b78803138fbaaa3f61b7dc13439ce3e08c95",
 "NodeInfo": "enode://0955966accd8f36256e876790c9b66098675f7ac6bfc10b805d7356d66844cf696902b8dadb62c44cdb783db69197ebacc709ab1908229fe7e13be3f1eae35fe@0.0.0.0:30303?discport=0"
}
{
 "Address": "0x68795d3e326b553dc8b2c5739b87a9cb827037c8",
 "Nodekey": "9f0e0b268671c29c43a0976faa7e08fd20aae24219ad1db6dfc7e645413600c1",
 "NodeInfo": "enode://a76bf5be8ddd1b1b9bd8d46e5947ccef9c1ce492d4e8fe800e234e61be67a0dbd586e33afb4e17998dc53fa2ea5c72a8a0544c7baae45fc4c16c401c1de90a22@0.0.0.0:30303?discport=0"
}

static-nodes.json
[
 "enode://bd1049d796f1b71bef17d428ce8db5f22e478ecbeb9513c57e90d93ca1e9ec107f4f4b43585556ca8bb3ab630f1f6543d0d4147f5d890e1fde301b2af1fd7a08@0.0.0.0:30303?discport=0",
 "enode://6344e12a9b3f4fd5c154ee13ebe5351a5460a44302fd493a5e742adf8a294b6dc112fab1fa8ff19dde0027373c96c51ab6254153877c9fadabfc057624e522f0@0.0.0.0:30303?discport=0",
 "enode://0955966accd8f36256e876790c9b66098675f7ac6bfc10b805d7356d66844cf696902b8dadb62c44cdb783db69197ebacc709ab1908229fe7e13be3f1eae35fe@0.0.0.0:30303?discport=0",
 "enode://a76bf5be8ddd1b1b9bd8d46e5947ccef9c1ce492d4e8fe800e234e61be67a0dbd586e33afb4e17998dc53fa2ea5c72a8a0544c7baae45fc4c16c401c1de90a22@0.0.0.0:30303?discport=0"
]

genesis.json
{
 "config": {
 "chainId": 2017,
 "homesteadBlock": 1,
 "eip150Block": 2,
 "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
 "eip155Block": 3,
 "eip158Block": 3,
 "istanbul": {
 "epoch": 30000,
 "policy": 0
 }
 },
 "nonce": "0x0",
 "timestamp": "0x5a213583",
 "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f89af8549405a6245732c2350ba2ed64e840394c2239f8ad1f9497a80dc7a7e27f41ae006fa1253f1f105f77335c94f69faf33e8690e82b0043e9131e09bbbc394cbed9468795d3e326b553dc8b2c5739b87a9cb827037c8b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0",
 "gasLimit": "0x47b760",
 "difficulty": "0x1",
 "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
 "coinbase": "0x0000000000000000000000000000000000000000",
 "alloc": {
 "05a6245732c2350ba2ed64e840394c2239f8ad1f": {
 "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
 },
 "68795d3e326b553dc8b2c5739b87a9cb827037c8": {
 "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
 },
 "97a80dc7a7e27f41ae006fa1253f1f105f77335c": {
 "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
 },
 "f69faf33e8690e82b0043e9131e09bbbc394cbed": {
 "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
 }
 },
 "number": "0x0",
 "gasUsed": "0x0",
 "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}

你会看到不同的地址、enode 等。现在,创建 static-nodes.jsongenesis.jsonenode 密钥文件,并将前述内容放入其中。将节点密钥文件名设为 enode_id_1enode_id_2enode_id_3enode_id_1。将 enode URL 中的端口更改为 23000230012300223003

现在,让我们生成一个以太坊账户,并在创世块中分配一些以太币给它。以太币不是动态生成的,因此我们需要预先提供。使用以下命令生成以太坊账户:

./geth --datadir ./accounts account new

现在,将 accounts/keystore 目录中的文件名更改为 key1。然后将地址复制,放入 genesis 文件中,并分配一些余额。例如,如果我新生成的账户地址是 0x65d8c00633404140986e5e23aa9de8ea689c1d05,那么我的 genesis 文件内容将如下所示:

{
     "config": {
         "chainId": 2017,
         "homesteadBlock": 1,
         "eip150Block": 2,
         "eip150Hash": 
           "0x000000000000000000000000000000000000000000
            0000000000000000000000",
         "eip155Block": 3,
         "eip158Block": 3,
         "istanbul": {
             "epoch": 30000,
             "policy": 0
         }
     },
     "nonce": "0x0",
     "timestamp": "0x5a213583",
     "extraData": "0x00000000000000000000000000000000000000000000
       00000000000000000000f89af8549405a6245732c2350ba2ed64e840
       394c2239f8ad1f9497a80dc7a7e27f41ae006fa1253f1f105f77
       335c94f69faf33e8690e82b0043e9131e09bbbc394cbed9468795
       d3e326b553dc8b2c5739b87a9cb827037c8b841000000000000000
       0000000000000000000000000000000000000000000000
       0000000000000000000000000000000000000000000000
       00000000000000000000000c0",
     "gasLimit": "0x47b760",
     "difficulty": "0x1",
     "mixHash": "0x63746963616c2062797a616e74696e65206661756c7
       420746f6c6572616e6365",
     "coinbase": "0x0000000000000000000000000000000000000000",
     "alloc": {
         "05a6245732c2350ba2ed64e840394c2239f8ad1f": {
             "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
         },
         "68795d3e326b553dc8b2c5739b87a9cb827037c8": {
             "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
         },
         "97a80dc7a7e27f41ae006fa1253f1f105f77335c": {
             "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
         },
         "f69faf33e8690e82b0043e9131e09bbbc394cbed": {
             "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
         },
         "65d8c00633404140986e5e23aa9de8ea689c1d05": {
             "balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
         }
     },
     "number": "0x0",
     "gasUsed": "0x0",
     "parentHash": "0x00000000000000000000000000000
       00000000000000000000000000000000000"
}

起始节点

现在,在我们启动节点之前,我们需要初始化它们:为每个节点创建数据目录,将账户密钥复制到数据目录,复制验证者的 enode 密钥,并使用创世块引导区块链。

以下是为所有六个节点实现这些的命令:

#Configuring Node 1
mkdir -p qdata/node1/{keystore,geth}
cp accounts/keystore/key1 qdata/node1/keystore
cp static-nodes.json qdata/node1
cp enode_id_1 qdata/node1/geth/nodekey
./geth --datadir qdata/node1 init genesis.json 

#Configuring Node 2
mkdir -p qdata/node2/geth
cp static-nodes.json qdata/node2
cp enode_id_2 qdata/node2/geth/nodekey
./geth --datadir qdata/node2 init genesis.json 

#Configuring Node 3
mkdir -p qdata/node3/geth
cp static-nodes.json qdata/node3
cp enode_id_3 qdata/node3/geth/nodekey
./geth --datadir qdata/node3 init genesis.json 

#Configuring Node 4
mkdir -p qdata/node4/geth
cp static-nodes.json qdata/node4
cp enode_id_4 qdata/node4/geth/nodekey
./geth --datadir qdata/node4 init genesis.json 

#Configuring Node 5
mkdir -p qdata/node5/geth
cp static-nodes.json qdata/node5
./geth --datadir qdata/node5 init genesis.json 

#Configuring Node 6
mkdir -p qdata/node6/geth
cp static-nodes.json qdata/node6
./geth --datadir qdata/node6 init genesis.json 

上述命令是不言自明的。对于最后两个节点,我们没有生成任何 enode 密钥,因为 geth 如果不存在,则会自动生成一个。现在,运行以下命令启动 Quorum 节点。在新的 shell 窗口中运行每个命令:

./geth --datadir qdata/node1 --mine --port 23000 --ipcpath "./geth.ipc" --istanbul.requesttimeout 5000 --istanbul.blockperiod 1 --istanbul.blockpausetime 20 console
./geth --datadir qdata/node2 --mine --port 23001 --ipcpath "./geth.ipc" --istanbul.requesttimeout 5000 --istanbul.blockperiod 1 --istanbul.blockpausetime 20 console
./geth --datadir qdata/node3 --mine --port 23002 --ipcpath "./geth.ipc" --istanbul.requesttimeout 5000 --istanbul.blockperiod 1 --istanbul.blockpausetime 20 console
./geth --datadir qdata/node4 --mine --port 23003 --ipcpath "./geth.ipc" --istanbul.requesttimeout 5000 --istanbul.blockperiod 1 --istanbul.blockpausetime 20 console
./geth --datadir qdata/node5 --port 23004 --ipcpath "./geth.ipc" console
./geth --datadir qdata/node6 --port 23005 --ipcpath "./geth.ipc" console

下面是我们刚刚传递的不同选项的含义:

  • 在运行验证者时需要 --mine

  • --istanbul.requesttimeout 是最大区块时间(默认值:10000ms)。

  • --istanbul.blockperiod 是最小区块时间(默认值:1s)。

  • --istanbul.blockpausetime 是前一个区块中没有交易时的暂停时间。值应大于 istanbul.blockperiod(默认值:2s)。

要获取网络中所有验证者的列表,您可以使用 istanbul.getValidators() API。

动态添加或移除验证者

让我们首先看看如何动态添加新的验证节点。要添加验证节点,我们首先需要生成新验证节点的节点密钥和地址。运行以下命令生成它:

./istanbul setup --num 1 --nodes --verbose

这是我们之前使用的相同命令。现在,我们不需要 genesis 文件或 static-nodes.json 文件。我们只需要节点密钥和地址。创建一个名为 node_id_5 的文件,并将节点密钥放入其中。运行以下命令初始化新的验证者:

#Configuring Node 7
mkdir -p qdata/node7/geth
cp static-nodes.json qdata/node7
cp enode_id_5 qdata/node7/geth/nodekey
./geth --datadir qdata/node7 init genesis.json

现在,在上述命令成功运行后,是时候让 (2F+ 1) 其他验证者同意插入新的验证者了。为此,在所有其他验证者中运行以下命令:

istanbul.propose("0x349ec6eefe8453a875c4905f5581ea792806a3e5", true)

将第一个参数替换为您获得的新验证节点地址。现在,使用以下命令启动新的验证节点:

./geth --datadir qdata/node7 --mine --port 23006 --ipcpath "./geth.ipc" --istanbul.requesttimeout 5000 --istanbul.blockperiod 1 --istanbul.blockpausetime 20 console

现在,您可以运行 istanbul.getValidators() 来检查网络中所有验证者的列表。现在应该有五个。让我们从网络中移除一个验证者。假设我们想要移除第一个验证者。在第一个验证者的控制台中运行 eth.coinbase 找到其唯一地址。然后,在 (2F + 1) 个验证者中运行以下命令以从网络中移除第一个验证者:

istanbul.propose("0x05a6245732c2350ba2ed64e840394c2239f8ad1f", false)

在此处,使用您生成的第一个验证节点的地址替换第一个参数。

在移除或添加验证节点时,如果某个验证节点宕机,那么一旦它重新运行起来,它将自动了解到这些更改。

概要

在本章中,我们从以太坊区块链的基础知识开始,然后深入探讨了 Quorum 的特性和共识协议。然后,通过设置星座、Raft 和 IBFT 网络,我们第一次实践了 Quorum。现在,您应该对设置网络的过程感到满意了。下一步是学习编写智能合约,并部署我们的第一个智能合约。我们将在下一章中实现这一点。

第三章:编写智能合约

在上一章中,我们了解了 Quorum 的工作原理以及各种共识协议是如何保护它的。现在我们了解了 Quorum 的工作原理,让我们继续编写智能合约。Quorum 智能合约可以使用许多语言编写;最流行的是Solidity。在本章中,我们将学习 Solidity,并构建一个企业可以用来数字签署文件的 DApp。

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

  • Solidity 源文件的布局

  • 理解 Solidity 数据类型

  • 特殊变量和合约函数

  • 控制结构

  • 合约的结构和特性

  • 编译和部署合约

本章与作者之前的书籍项目区块链中的章节相同。这不是第二版的书籍,它被用来向读者解释基本概念。

Solidity 源文件

Solidity 源文件的识别方法是通过 .sol 扩展名。它有各种版本,就像通常的编程语言一样。在撰写本书时,最新版本是 0.4.17

在源文件中,您可以使用 pragma Solidity 指令来指定编写代码的编译器版本。例如:

pragma Solidity ⁰.4.17;

需要注意的是,源文件不会在早于 0.4.17 或晚于 0.5.0(此第二个条件使用 ^ 添加)的编译器版本下编译。编译器版本在 0.4.170.5.0 之间的情况最有可能包含 bug 修复,并且不太可能破坏任何内容。

我们可以为编译器版本指定更复杂的规则;表达式遵循 npm 使用的规则。

智能合约的结构

A 类似于一个类。它可以有函数、修改器、状态变量、事件、结构体和枚举。合约也支持继承。您可以通过在编译时复制代码来实现继承。智能合约也可以是多态的。

以下是一个智能合约的示例:

contract Sample
{
     //state variables 
     uint256 data;
     address owner;

     //event definition
     event logData(uint256 dataToLog);

     //function modifier
     modifier onlyOwner() {
         if (msg.sender != owner) throw;
         _; 
    }

     //constructor
     function Sample(uint256 initData, address initOwner){
         data = initData;
         owner = initOwner;
     }

     //functions
     function getData() returns (uint256 returnedData){
         return data;
     }
     function setData(uint256 newData) onlyOwner{
         logData(newData);
         data = newData;
     }
}

让我们看看上述代码如何工作:

  • 首先,我们使用 contract 关键字声明了一个合约。

  • 接下来,我们声明了两个状态变量:data 保存一些数据;owner 保存了他们的以太坊钱包的地址,也就是合约部署的地址。状态变量构成智能合约的状态,并存储在智能合约的存储中。智能合约的存储位于数据库中。

  • 然后,我们定义了事件。事件用于客户端通知。我们的事件将在数据更改时触发。所有事件都保留在区块链中。

  • 接下来,我们定义了一个修改器函数。修改器在执行函数之前自动检查条件。我们的修改器检查合约所有者是否是调用函数的人。如果不是,则会抛出异常。

  • 在此之后,我们有了合约构造函数。它在部署合约时调用。构造函数用于初始化状态变量。

  • 最后,我们定义了两种方法。第一种方法获取数据状态变量的值,第二种方法更改数据值。

在更深入研究智能合约功能之前,我们必须学习与 Solidity 相关的一些重要事项。之后,我们将回到合约。

Solidity 中的数据位置

与其他编程语言不同,Solidity 的变量根据上下文存储在内存和数据库中。

总是有一个默认位置,但可以通过附加 storage 或 memory 来覆盖复杂类型的数据,例如字符串、数组和结构体。Memory 是函数参数(包括 return 参数)的默认值,而 storage 适用于局部和状态变量(显然)。

数据位置很重要,因为它们会改变赋值的行为:

  • 在存储变量和内存变量之间的赋值中,始终会创建独立的副本。但是,从一个内存存储的复杂类型赋值给另一个内存存储的复杂类型时,不会创建副本。

  • 对状态变量进行赋值时,始终会创建独立的副本(即使来自其他状态变量)。

  • 存储在内存中的复杂类型不能赋值给局部存储变量。

  • 如果状态变量赋给局部存储变量,那么局部存储变量将指向状态变量;基本上,局部存储变量充当指针。

不同类型的数据

Solidity 是一种静态类型语言;变量持有的数据类型需要预定义。所有变量的位默认都被赋值为零。在 Solidity 中,变量是在函数范围内生效;也就是说,无论在函数的任何地方声明的变量都将在整个函数范围内生效。

Solidity 提供了以下数据类型:

  • 最简单的数据类型是 bool。它可以存储 truefalse

  • uint8uint16uint24,一直到 uint256 用于存储 8 位、16 位、24 位,一直到 256 位的无符号整数。同样地,int8int16 一直到 int256 用于存储 8 位、16 位,一直到 256 位的有符号整数。uintintuint256int256 的别名。ufixedfixed代表分数。ufixed0x8ufixed0x16,一直到 ufixed0x256 用于存储 8 位、16 位,一直到 256 位的无符号分数。类似地,fixed0x8fixed0x16,一直到 fixed0x256 用于存储 8 位、16 位,一直到 256 位的有符号分数。如果我们有一个需要超过 256 位的数字,那么将使用 256 位数据类型,此时将存储数字的近似值。

  • 地址(Address)用于存储最多 20 字节的值,通过分配十六进制字面量。它用于存储以太坊地址。您可以在 Solidity 中使用 0x 前缀,将十六进制编码的值赋给变量。

数组

Solidity 支持通用和字节数组,固定大小和动态数组,以及多维数组。

bytes1bytes2bytes3,一直到 bytes32 都是字节数组的类型。我们将使用字节表示 bytes1

这是一些通用数组语法的示例:

 contract sample{
//dynamic size array
//wherever an array literal is seen a new array is created. If the //array literal is in state, then it's stored in storage and if it's //found inside function, then its stored in memory
//Here myArray stores [0, 0] array. The type of [0, 0] is decided based //on its values.
//Therefore, you cannot assign an empty array literal.
     int[] myArray = [0, 0];

    function sample(uint index, int value){
         //index of an array should be uint256 type
         myArray[index] = value;

         //myArray2 holds pointer to myArray
         int[] myArray2 = myArray;
//a fixed size array in memory
//here we are forced to use uint24 because 99999 is the max value and //24 bits is the max size required to hold it.
//This restriction is applied to literals in memory because memory is //expensive. As [1, 2, 99999] is of type uint24, myArray3 also has to //be the same type to store pointer to it.
         uint24[3] memory myArray3 = [1, 2, 99999]; //array literal

//throws exception while compiling as myArray4 cannot be assigned to //complex type stored in memory
         uint8[2] myArray4 = [1, 2];
     }
}

以下是您应该了解的一些关于数组的重要事项:

  • 数组还具有 length 属性,可用于查找数组的长度。还可以将 value 分配给 length 属性以更改数组大小。但是,内存中的数组或非动态数组无法调整大小。

  • 如果尝试访问动态数组的未设置的 index,则会引发异常。

字符串

在 Solidity 中,可以通过两种方式创建字符串:使用 bytesstringbytes 用于创建原始字符串,而 string 用于创建 UTF-8 字符串。字符串的长度始终是动态的。

以下是显示 string 语法的示例:

contract sample {
//wherever a string literal is seen, a new string is created. If the //string literal is in state, then it's stored in storage and if it's //found inside function, then its stored in memory
     //Here myString stores "" string.
     string myString = ""; //string literal
     bytes myRawString;

     function sample(string initString, bytes rawStringInit){
         myString = initString;

         //myString2 holds a pointer to myString
         string myString2 = myString;

         //myString3 is a string in memory
         string memory myString3 = "ABCDE";

         //here the length and content changes
         myString3 = "XYZ";
         myRawString = rawStringInit;

         //incrementing the length of myRawString
         myRawString.length++;

         //throws exception while compiling
         string myString4 = "Example";

         //throws exception while compiling
         string myString5 = initString;
    }
}

结构体

Solidity 结构体。以下是 struct 语法的示例:

contract sample{
     struct myStruct {
         bool myBool;
         string myString;
     }

     myStruct s1;

    //wherever a struct method is seen, a new struct is created. If 
    //the struct method is in state, then it's stored in storage
    //and if it's found inside function, then its stored in memory
     myStruct s2 = myStruct(true, ""); //struct method syntax

     function sample(bool initBool, string initString){
         //create an instance of struct
         s1 = myStruct(initBool, initString);

        //myStruct(initBool, initString) creates an instance in memory
         myStruct memory s3 = myStruct(initBool, initString);
     }
}

枚举

Solidity 枚举。以下是 enum 语法的示例:

contract sample {
     //The integer type which can hold all enum values and is the
     //smallest is chosen to hold enum values
     enum OS { Windows, Linux, OSX, UNIX }

     OS choice;

     function sample(OS chosen){
         choice = chosen;
     }

     function setLinuxOS(){
         choice = OS.Linux;
     }

     function getChoice() returns (OS chosenOS){
         return choice;
     } 
}

映射

哈希表 是一种映射数据类型。由于映射只能存在于存储中,因此它们被声明为状态变量。您可以将映射看作具有 keyvalue 对的数据结构。key 实际上不会被存储;相反,将 keykeccak256 哈希用于查找 value。映射没有长度,并且无法分配给另一个映射。

以下是创建和使用 mapping 的示例:

contract sample{
     mapping (int => string) myMap;

     function sample(int key, string value){
         myMap[key] = value;

         //myMap2 is a reference to myMap
         mapping (int => string) myMap2 = myMap;
     }
}

delete 运算符

delete 运算符可以应用于任何变量以将其重置为其默认值。默认值是所有位都分配为零。

如果我们对动态数组应用 delete,它将删除所有元素并使长度变为零。如果我们对静态数组应用 delete,它的所有索引都会被重置。我们也可以对特定的索引应用 delete,以重置它们。

然而,如果您将 delete 应用于映射类型,则不会发生任何事情。但是,如果您将 delete 应用于映射的 key,则与 key 关联的值将被删除。

让我们看看 delete 运算符的工作原理,如下所示:

contract sample { 

    struct Struct { 
        mapping (int => int) myMap; 
        int myNumber; 
    } 

    int[] myArray; 
    Struct myStruct; 

    function sample(int key, int value, int number, int[] array) { 

        //maps cannot be assigned so while constructing struct we
        // ignore the maps 
        myStruct = Struct(number); 

        //here set the map key/value 
        myStruct.myMap[key] = value; 

        myArray = array; 
    } 

    function reset(){ 

        //myArray length is now 0 
        delete myArray; 

        //myNumber is now 0 and myMap remains as it is 
        delete myStruct; 
    } 

    function deleteKey(int key){ 

        //here we are deleting the key 
        delete myStruct.myMap[key]; 
    } 
} 

基本类型之间的转换

除了数组、字符串、结构体、枚举和映射之外的所有内容都被称为 基本类型

如果我们对不同类型的操作数应用运算符,编译器会尝试将其中一个操作数隐式转换为另一个操作数的类型。一般来说,如果在语义上有意义且不会丢失信息,那么值类型之间的隐式转换是可能的:uint8 可转换为 uint16int128 可转换为 int256,但 int8 无法转换为 uint256(因为 uint256 无法容纳,例如,-1)。此外,无符号整数可以转换为相同或更大尺寸的字节,但反之则不行。任何可以转换为 uint160 的类型也可以转换为地址。

Solidity 还支持显式转换。如果编译器不允许两种数据类型之间的隐式转换,您可以选择显式转换。我们建议避免显式转换,因为它可能会给您带来意外的结果。

显式转换的以下示例,如下所示:

uint32 a = 0x12345678; 
uint16 b = uint16(a); // b will be 0x5678 now 

在这里,我们将uint32类型显式转换为uint16,即将大类型转换为小类型;因此,高阶位被截断。

使用 var

Solidity 提供了var关键字来声明变量。在这种情况下,变量的类型是动态决定的,取决于分配给它的第一个值。一旦分配了一个值,类型就固定了;如果您给它分配另一种类型,它将导致类型转换。

让我们看看var的工作原理,如下所示:

int256 x = 12;

//y type is int256 
var y = x;

uint256 z= 9;

//exception because implicit conversion not possible 
y = z;

请注意,在定义数组和映射时不能使用var。它也不能用于定义函数参数和状态变量。

控制结构

Solidity 支持if...elsedo...whileforbreakcontinuereturn?:控制结构。

这里有一个结构:

contract sample{ 
    int a = 12; 
    int[] b; 

    function sample() 
    { 
        //"==" throws exception for complex types 
        if(a == 12) 
        { 
        } 
        else if(a == 34) 
        { 
        } 
        else 
        { 
        } 

        var temp = 10; 

        while(temp < 20) 
        { 
            if(temp == 17) 
            { 
                break; 
            } 
            else 
            { 
                continue; 
            } 

            temp++; 
        } 

        for(var iii = 0; iii < b.length; iii++) 
        { 

        } 
    } 
} 

使用 new 操作符创建合同

合同可以使用new关键字创建新的合同。必须了解被创建合同的完整代码。

让我们演示一下,如下所示:

contract sample1 
{ 
    int a; 

    function assign(int b) 
    { 
        a = b; 
    } 
} 

contract sample2{ 
    function sample2() 
    { 
        sample1 s = new sample1(); 
        s.assign(12); 
    } 
}

异常

在某些情况下,异常会自动抛出。您可以使用assert()revert()require()来抛出手动异常。异常会停止并撤销当前正在执行的调用(即,对状态和余额的所有更改都将被撤消)。在 Solidity 中,尚不可能捕获异常。

以下三行是 Solidity 中抛出异常的不同方式:

if(x != y) { revert(); }

//In assert() and require(), the conditional statement is an inversion //to "if" block's condition, switching the comparison operator != to ==
assert(x == y);
require(x == y);

assert()将消耗所有 gas,而require()revert()将退还剩余的 gas。

Solidity 不支持返回异常的原因,但预计会很快支持。您可以访问github.com/ethereum/solidity/issues/1686问题进行更新。然后,您将能够编写revert("Something bad happened")require(condition, "Something bad happened")

外部函数调用

Solidity 有两种类型的函数调用:内部和外部。内部函数调用是指函数调用同一合同中的另一个函数。外部函数调用是指函数调用另一个合同的函数。

以下是一个示例:

contract sample1 
{ 
    int a; 

    //"payable" is a built-in modifier 
    //This modifier is required if another contract is sending 
    // Ether while calling the method 
    function sample1(int b) payable 
    { 
        a = b; 
    } 

    function assign(int c) 
    { 
        a = c; 
    } 

    function makePayment(int d) payable 
    { 
        a = d; 
    } 
} 

contract sample2{ 

    function hello() 
    { 
    } 

    function sample2(address addressOfContract) 
    { 
        //send 12 wei while creating contract instance 
        sample1 s = (new sample1).value(12)(23); 

        s.makePayment(22); 

        //sending Ether also 
        s.makePayment.value(45)(12); 

        //specifying the amount of gas to use 
        s.makePayment.gas(895)(12); 

        //sending Ether and also specifying gas 
        s.makePayment.value(4).gas(900)(12); 

        //hello() is internal call whereas this.hello() is 
        external call 
        this.hello(); 

        //pointing a contract that's already deployed 
        sample1 s2 = sample1(addressOfContract); 

        s2.makePayment(112); 

    } 
}

使用this关键字进行的调用称为外部调用。函数内部的this关键字表示当前合同实例。

合同的特性

现在是深入研究合同的时候了。让我们从一些新特性开始,然后我们将更深入地了解我们已经看到的特性。

可见性

状态变量或函数的可见性定义了谁可以看到它。可见性有四种类型:externalpublicinternalprivate

默认情况下,函数的可见性为public,状态变量的可见性为internal。让我们看看这些可见性函数意味着什么:

  • external:外部函数只能从其他合约或通过交易调用。例如,我们无法在内部调用一个f外部函数:f()将不起作用,但this.f()会起作用。我们也不能将external可见性应用于状态变量。

  • public:公共函数和状态变量可以以各种方式访问。编译器生成的访问器函数都是public状态变量。我们不能创建自己的访问器。实际上,它只生成getter,而不是setter

  • internal:内部函数和状态变量只能在内部访问,即在当前合约和继承它的合约中。我们不能使用this来访问它。

  • private:私有函数和状态变量与内部函数类似,只是不能被继承合约访问。

这是一个代码示例,用于演示可见性和访问器:

contract sample1 
{ 
    int public b = 78; 
    int internal c = 90; 

    function sample1() 
    { 
        //external access 
        this.a(); 

        //compiler error 
        a(); 

        //internal access 
        b = 21; 

        //external access 
        this.b; 

        //external access 
        this.b(); 

        //compiler error 
        this.b(8); 

        //compiler error 
        this.c(); 

        //internal access 
        c = 9; 
    } 

    function a() external 
    { 

    } 
} 

contract sample2 
{ 
    int internal d = 9; 
    int private e = 90; 
} 

//sample3 inherits sample2 
contract sample3 is sample2 
{ 
    sample1 s; 

    function sample3() 
    { 
        s = new sample1(); 

        //external access 
        s.a(); 

        //external access 
        var f = s.b; 

        //compiler error as accessor cannot used to assign a value 
        s.b = 18; 

        //compiler error 
        s.c(); 

        //internal access 
        d = 8; 

        //compiler error 
        e = 7; 
    } 
} 

函数修饰符

我们已经看到了函数修饰符是什么,并且我们编写了一个基本版本。现在让我们详细看一下它。

修饰符由子合约继承,并且它们也可以被子合约覆盖。可以通过在空格分隔的列表中指定它们来向函数应用多个修饰符,并且它们将按顺序进行评估。您还可以向修饰符传递参数。

在修饰符内部,下一个修饰符主体或函数主体,以后出现的,被插入到_;出现的位置。

让我们看一个函数修饰符的复杂代码示例,如下所示:

contract sample 
{ 
    int a = 90; 

    modifier myModifier1(int b) { 
        int c = b; 
        _; 
        c = a; 
        a = 8; 
    } 

    modifier myModifier2 { 
        int c = a; 
        _; 
    } 

    modifier myModifier3 { 
        a = 96; 
        return; 
        _; 
        a = 99; 
    } 

    modifier myModifier4 { 
        int c = a; 
        _; 
    } 

    function myFunction() myModifier1(a) myModifier2 myModifier3 returns (int d) 
    { 
        a = 1; 
        return a; 
    } 
}

这是myFunction()的执行方式:

int c = b; 
    int c = a; 
        a = 96; 
        return; 
            int c = a; 
                a = 1; 
                return a; 
        a = 99; 
c = a; 
a = 8; 

在这里,当你调用myFunction方法时,它将返回0。但之后,当你尝试访问状态变量a时,你将得到8

return 在修饰符或函数体中立即离开整个函数,返回值被分配给需要的变量。

在函数的情况下,return后的代码在调用方的代码执行完成后执行。而在修饰符的情况下,在前一个修饰符中的_;后的代码在调用方的代码执行完成后执行。在上述示例中,第五、六和七行永远不会执行。在第四行之后,执行直接从第八到第十行开始。

修饰符内部的return不能与任何值关联。它总是返回零位。

回退函数

回退函数是合约可以拥有的唯一无名称的函数。此函数不能有参数,也不能返回任何内容。如果没有其他函数匹配给定的函数标识符,则在调用合约时执行该函数。

这个函数也会在合约在没有任何函数调用的情况下接收以太币时执行;也就是说,交易将以太币发送到合约并不调用任何方法。在这样的情况下,通常只有很少的气体可用于函数调用(精确地说是 2,300 气体),因此将回退函数尽可能地廉价是很重要的。

当合约收到以太币但没有定义回退函数时,它们会抛出异常,从而将以太币退回。因此,如果你希望你的合约接收以太坊,你必须实现一个回退函数。

下面是一个回退函数的例子:

contract sample 
{ 
    function() payable 
    { 
        //Note how much Ether has been sent and by whom 
    } 
} 

继承

Solidity 支持通过复制代码来实现多重继承,包括多态性。即使一个合约从多个其他合约继承,区块链上也只会创建一个合约。此外,父合约的代码始终会被复制到最终的合约中。

让我们来回顾一个继承的例子:

contract sample1 
{ 
    function a(){} 

    function b(){} 
} 

//sample2 inherits sample1 
contract sample2 is sample1 
{ 
    function b(){} 
} 

contract sample3 
{ 
    function sample3(int b) 
    { 

    } 
} 

//sample4 inherits from sample1 and sample2 
//Note that sample1 is also a parent of sample2; yet there is only a
// single instance of sample1 
contract sample4 is sample1, sample2 
{ 
    function a(){} 

    function c(){ 

        //this executes the "a" method of sample3 contract 
        a(); 

        //this executes the "a" method of sample1 contract 
        sample1.a(); 

        //calls sample2.b() because it is last in the parent 
        contracts list and therefore it overrides sample1.b() 
        b(); 
    } 
} 

//If a constructor takes an argument, it needs to be provided at 
//the constructor of the child contract. 
//In Solidity, child constructor does not call parent constructor,
// instead parent is initialized and copied to child 
contract sample5 is sample3(122) 
{ 

} 

超级关键字

super 关键字用于引用最终继承链中的下一个合约。以下是一个例子,帮助你更好地理解:

contract sample1 
{ 
} 

contract sample2 
{ 
} 

contract sample3 is sample2 
{ 
} 

contract sample4 is sample2 
{ 
} 

contract sample5 is sample4 
{ 
    function myFunc() 
    { 
    } 
} 

contract sample6 is sample1, sample2, sample3, sample5 
{ 
    function myFunc() 
    { 
        //sample5.myFunc() 
        super.myFunc(); 
    } 
} 

关于 sample6 合约的最终继承链是 sample6, sample5, sample4, sample2, sample3, sample1。继承链以最派生的合约开始,以最不派生的合约结束。

抽象合约

抽象合约是仅包含函数原型而不包含实现的合约。它们无法被编译(即使它们包含了已实现的函数和未实现的函数)。如果一个合约继承自一个抽象合约并且没有通过覆盖实现所有未实现的函数,那么它本身也会变成抽象的。

提供抽象合约的原因是为了让编译器知道接口。这对于引用已部署的合约并调用其函数是有用的。

让我们通过下面的例子来演示:

contract sample1 
{ 
    function a() returns (int b); 
} 

contract sample2 
{ 
    function myFunc() 
    { 
        sample1 s = 
          sample1(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970); 

        //without abstract contract this wouldn't have compiled 
        s.a(); 
    } 
}

库与合约类似,但它们只在特定地址部署一次,它们的代码被各种合约重复使用。这意味着如果库函数被调用,它们的代码会在调用合约的上下文中执行;因此,this 指向调用合约,并且特别允许访问调用合约的存储。由于库是一个孤立的源代码片段,它只能在显式提供它们的情况下访问调用合约的状态变量(否则它无法命名它们)。

库可以包含结构体和枚举,但它们不能有状态变量。它们不支持继承,也不能接收以太币。

一旦 Solidity 库被部署到区块链上,任何人都可以使用它,只要知道它的地址并且有源代码(只有原型或完整实现)。Solidity 编译器需要源代码,以便确保正在访问的方法确实存在于库中。

下面是一个示例:

library math 
{ 
    function addInt(int a, int b) returns (int c) 
    { 
        return a + b; 
    } 
} 

contract sample 
{ 
    function data() returns (int d) 
    { 
        return math.addInt(1, 2); 
    } 
} 

库的地址无法添加到合约源代码中。我们需要在编译期间向编译器提供库地址。

库有许多用例。两个主要用例如下:

  • 如果你有几个合约具有一些公共代码,你可以将该公共代码作为库部署。这将节省燃气,这也取决于合约的大小。因此,我们可以将库视为使用它的合约的基础合约。使用基础合约而不是库来拆分公共代码将不会节省燃气,因为 Solidity 中的继承是通过复制代码实现的。因为库被认为是基础合约,所以库中具有内部可见性的函数会被复制到使用它的合约中。否则,具有库内部可见性的函数无法被使用库的合约调用,因为需要进行外部调用。具有内部可见性的函数无法使用外部调用调用。此外,库中的结构和枚举会被复制到使用库的合约中。

  • 库可以用来为数据类型添加成员函数。

仅包含内部函数和/或结构/枚举的库不需要部署,因为库中的所有内容都会复制到使用它的合约中。

使用 for

using A for B; 指令可用于将库函数(来自库 A)附加到任何类型 B 上。这些函数将以调用它们的对象作为第一个参数。

using A for *; 的效果是将库 A 的函数附加到所有类型上。

下面是一个演示 for 的示例:

library math 
{ 
    struct myStruct1 { 
        int a; 
    } 

    struct myStruct2 { 
        int a; 
    } 

    //Here we have to make 's' location storage so that we 
    //get a reference. 
    //Otherwise addInt will end up accessing/modifying a 
    //different instance of myStruct1 than the one on which its invoked 
    function addInt(myStruct1 storage s, int b) returns (int c) 
    { 
        return s.a + b; 
    } 

    function subInt(myStruct2 storage s, int b) returns (int c) 
    { 
        return s.a + b; 
    } 
} 

contract sample 
{ 
    //"*" attaches the functions to all the structs 
    using math for *; 
    math.myStruct1 s1; 
    math.myStruct2 s2; 

    function sample() 
    { 
        s1 = math.myStruct1(9); 
        s2 = math.myStruct2(9); 

        s1.addInt(2); 

        //compiler error as the first parameter of addInt is
        //of type myStruct1 so addInt is not attached to myStruct2 
        s2.addInt(1); 
    } 
} 

返回多个值

Solidity 允许函数返回多个值。让我们演示一下,如下所示:

contract sample 
{ 
    function a() returns (int a, string c) 
    { 
        return (1, "ss"); 
    } 

    function b() 
    { 
        int A; 
        string memory B; 

        //A is 1 and B is "ss" 
        (A, B) = a(); 

        //A is 1 
        (A,) = a(); 

        //B is "ss" 
        (, B) = a(); 
    } 
} 

导入其他 Solidity 源文件

Solidity 允许一个源文件导入其他源文件。下面是一个示例来演示这一点:

//This statement imports all global symbols from "filename" (and //symbols imported therein) to the current global scope. "filename" can //be an absolute or relative path. It can only be an HTTP URL 
//import "filename"; 

//creates a new global symbol symbolName whose members are all the //global symbols from "filename". 
import * as symbolName from "filename"; 

//creates new global symbols alias and symbol2 which reference symbol1 //and symbol2 from "filename", respectively. 
import {symbol1 as alias, symbol2} from "filename"; 

//this is equivalent to import * as symbolName from "filename";. 
import "filename" as symbolName; 

全局可用变量

有一些特殊的变量和函数总是全局存在。我们将在接下来的章节中讨论它们。

区块和交易属性

区块和交易属性如下所示:

  • block.blockhash(uint blockNumber) returns (bytes32): 给定区块的哈希仅适用于最近的 256 个区块。

  • block.coinbase (address): 当前区块的矿工地址。

  • block.difficulty (uint): 当前区块的难度。

  • block.gaslimit (uint): 当前区块的燃气限制。它定义了整个区块中所有交易允许消耗的最大燃气量。其目的是保持区块传播和处理时间低,从而使网络足够去中心化。矿工有权将当前区块的燃气限制设置为上一个区块燃气限制的~0.0975%(1/1,024),因此结果燃气限制应该是矿工偏好的中位数。

  • block.number (uint): 当前区块的编号。

  • block.timestamp (uint): 当前区块的时间戳。

  • msg.data (字节): 完整的调用数据包含了交易调用的函数及其参数。

  • msg.gas (uint): 剩余的 gas。

  • msg.sender (地址): 消息的发送者(当前调用)。

  • msg.sig (bytes4): 调用数据的前四个字节(函数标识符)。

  • msg.value (uint): 随消息发送的 wei 的数量。

  • now (uint): 当前区块的时间戳(别名为 block.timestamp)。

  • tx.gasprice (uint): 交易的 gas 价格。

  • tx.origin (地址): 交易的发送者(完整的调用链)。

地址类型相关的变量

地址类型相关的变量如下:

  • <address>.balance (uint256): 地址中 wei 的余额。

  • <address>.send(uint256 金额) returns (bool): 将指定金额的 wei 发送到地址;失败时返回false。即使执行失败,当前合同也不会因异常而停止。

  • <address>.transfer(uint256 金额): 向地址发送 wei。如果执行耗尽 gas 或失败,则以太转账将被撤销,并且当前合同将因异常而停止。

合同相关的变量

合同相关的变量如下:

  • this: 当前合同,可以显式转换为地址类型

  • selfdestruct(address 收款人): 销毁当前合同,并将其资金发送到指定地址。

以太单位

字面数字可以附加weifinneyszaboether后缀,以在以太币的子单位之间进行转换,其中以太币货币数字没有后缀被假定为 wei。例如,2 Ether == 2000 finney计算结果为true

存在性、完整性和所有权证明合同

如今,企业正在使用电子签名解决方案签署协议。然而,这些文件的详细信息存储在可以轻松更改的数据库中,因此不能用于审计目的。区块链可以通过将区块链集成为这些电子签名系统的解决方案来解决此问题。

让我们编写一个 Solidity 合同,可以证明文件所有权而不泄露实际文件。它可以证明文件在特定时间存在,并检查文件的完整性。

企业可以使用此解决方案在区块链上存储其协议的哈希。这样做的好处是可以证明协议的日期/时间、协议的实际条款等。

我们将通过将文件的哈希和所有者的名称存储为对来实现所有权的证明。所有者可以是创建协议的企业。另一方面,我们将通过将文件的哈希和区块时间戳存储为对来实现存在性的证明。最后,存储哈希本身证明了文件的完整性。如果文件被修改,其哈希将更改,合同将无法找到文件,从而证明文件已被修改。

我们将使用 Quorum 的私有交易,因为实体之间签署的协议对它们是私有的,细节不会暴露给其他实体。尽管只有文件的哈希将被暴露,但其他实体知道一个实体签署了多少协议仍然不是一个好主意。

以下是实现所有这些的智能合约代码:

contract Proof 
{ 
 struct FileDetails 
 { 
 uint timestamp; 
 string owner; 
 } 

 mapping (string => FileDetails) files; 

 event logFileAddedStatus(bool status, uint timestamp, 
   string owner, string fileHash); 

 //this is used to store the owner of file at the block timestamp 
 function set(string owner, string fileHash) 
 { 
 //There is no proper way to check if a key already exists, 
 //therefore we are checking for default value i.e., all bits are 0 
 if(files[fileHash].timestamp == 0) 
 { 
 files[fileHash] = FileDetails(block.timestamp, owner); 

 //we are triggering an event so that the frontend of our app
 //knows that the file's existence and ownership 
 //details have been stored 
 logFileAddedStatus(true, block.timestamp, owner, fileHash); 
 } 
 else 
 { 
 //this tells the frontend that the file's existence and 
 //ownership details couldn't be stored because the 
 //file's details had already been stored earlier 
 logFileAddedStatus(false, block.timestamp, owner, fileHash); 
 } 
 } 

 //this is used to get file information 
 function get(string fileHash) returns (uint timestamp, string owner) 
 { 
 return (files[fileHash].timestamp, files[fileHash].owner); 
 } 
} 

编译和部署合约

以太坊提供了 solc 编译器,该编译器提供了一个命令行界面来编译.sol文件。访问solidity.readthedocs.io/en/develop/installing-solidity.html#binary-packages获取安装说明,并访问Solidity.readthedocs.io/en/develop/using-the-compiler.html获取使用说明。我们不会直接使用 solc 编译器;相反,我们将使用浏览器 Solidity。浏览器 Solidity 是一个适用于小型合约的集成开发环境(IDE)。

现在,让我们使用浏览器 Solidity 编译上述合约。了解更多信息,请访问Ethereum.github.io/browser-Solidity/。您还可以下载用于离线使用的浏览器 Solidity 源代码:github.com/Ethereum/browser-Solidity/tree/gh-pages

使用浏览器 Solidity 的一个主要优势是它提供了一个编辑器,并生成部署合约的代码。

在编辑器中,复制并粘贴上述合约代码。您会看到它编译并给出了使用 Geth 交互式控制台部署它的 web3.js 代码。

没有privateFor属性时,您将获得以下输出:

var proofContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"fileHash","type":"string"}],"name":"get","outputs":[{"name":"timestamp","type":"uint256"},{"name":"owner","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"string"},{"name":"fileHash","type":"string"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"bool"},{"indexed":false,"name":"timestamp","type":"uint256"},{"indexed":false,"name":"owner","type":"string"},{"indexed":false,"name":"fileHash","type":"string"}],"name":"logFileAddedStatus","type":"event"}]); 
var proof = proofContract.new( 
  { 
    from: web3.eth.accounts[0], 
    data: '0x606060.......', 
    gas: 4700000,
  privateFor: ['CGXyBlYOGgU4fZ7n8dVLaTW24p+ZOF8kSiUJkQCUABk=',
    'zumojc44Dge0juFgph4xzqOUyNVw+QNZUaY7wOL0P0o='] 
  }, function (e, contract){ 
   console.log(e, contract); 
  if (typeof contract.address !== 'undefined') { 
    console.log('Contract mined! address: ' + contract.address + '
      transactionHash: ' + contract.transactionHash); 
  } 
}) 

data表示合约的编译版本(字节码),以太坊虚拟机(EVM)可以理解。源代码首先被转换为操作码,然后转换为字节码。每个操作码都与gas相关联。

web3.eth.contract的第一个参数是 ABI 定义。ABI 定义包含所有方法的原型,并在创建交易时使用。

现在是部署智能合约的时候了。在进一步操作之前,请确保您启动了我们在上一章中创建的由三个节点组成的 raft 网络。我们将假设这三个节点来自三个不同的企业。还要确保您已启用 constellation,并复制所有 constellation 成员的公钥。在privateFor数组中,用您生成的公钥替换它们。在这里,我将私有智能合约对所有三个网络成员可见。

privateFor 仅在发送私有事务时使用。它被分配给一个接收者的 base64 编码的公钥数组。在上述代码中,在 privateFor 数组中,我只有两个公钥。这是因为发送者不必将其公钥添加到数组中。如果添加,那么将会引发错误。

在第一个节点的交互式控制台中,使用 personal.unlockAccount(web3.eth.accounts[0], "", 0) 来无限期地解锁以太坊账户。

在浏览器 Solidity 的右侧面板中,复制 web3 部署文本区域中的所有内容,然后添加 privateFor 并将其粘贴到第一个节点的交互式控制台中。现在按 Enter 键。您将首先获得事务哈希,等待一段时间后,事务被挖掘后您将获得合同地址。事务哈希是事务的哈希值,对于每个事务都是唯一的。每个部署的合同都有一个唯一的合同地址,用于在区块链中标识合同。

合同地址是从其创建者的地址(from 地址)和创建者发送的事务数量(事务 nonce)确定地计算出来的。这两个值都是 RLP 编码然后使用 keccak256 哈希算法进行哈希。我们将在后面了解更多关于事务 nonce 的内容。您可以在 github.com/Ethereum/wiki/wiki/RLP 了解更多关于递归长度前缀RLP)的信息。

现在让我们存储文件详细信息并检索它们。假设前两个实体已签署协议并希望将文件的详细信息存储在区块链上。将以下代码放置以广播一个事务以存储文件的详细信息:

var contract_obj = proofContract.at
  ("0x006c3e992b6e3f52e81560aa3ef6d66e1706b45c"); 
contract_obj.set.sendTransaction("Enterprise 1",
   "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    { 
 from: web3.eth.accounts[0], 
 privateFor: ['CGXyBlYOGgU4fZ7n8dVLaTW24p+ZOF8kSiUJkQCUABk=']
}, function(error, transactionHash){ 
  if (!err) 
    console.log(transactionHash); 
}) 

在这里,用您获得的合同地址替换合同地址。proofContract.at 方法的第一个参数是合同地址。在这里,我们没有提供 gas,这样会自动计算。最后,由于这是前两个实体之间的协议,第一个实体正在使用第二个实体的公钥发送交易,我们在 privateFor 属性中有第二个实体的公钥。

现在运行此代码以查找文件的详细信息:

contract_obj.get.call("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c
  a495991b7852b855"); 

您将获得此输出:

[1477591434, "Owner Name"] 

调用方法用于在 EVM 上调用合同的方法以及当前状态。它不广播事务。要读取数据,我们不需要广播,因为我们将有自己的区块链副本。

如果在节点 3 中运行上述代码,则不会得到任何细节,因为数据对第三个实体不可见。但第一个和第二个节点可以读取细节。在接下来的章节中,我们将更多地了解 web3.js。

摘要

在本章中,我们学习了 Solidity 编程语言。我们学习了数据位置,数据类型以及合约的高级特性。我们还学习了编译和部署智能合约的最快最简单的方法。现在你应该能够轻松编写智能合约了。

在下一章中,我们将为智能合约构建一个前端,这将使得部署智能合约和运行交易变得容易。

第四章:使用 web3.js 入门

在上一章中,我们学习了如何使用 Solidity 编写和部署智能合约。在本章中,我们将学习有关 web3.js 的知识,以及如何导入它,连接到 geth,并在 Node.js 或客户端 JavaScript 中使用它。我们还将学习如何使用 web3.js 构建一个网络客户端,用于上一章中创建的智能合约。

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

  • 在 Node.js 和客户端 JavaScript 中导入 web3.js

  • 连接到 geth

  • 探索 web3.js

  • 发现 web3.js 最常用的 API

  • 为所有权合约构建 Node.js 应用程序

本章与作者之前的书籍 Blockchain for Projects 中的章节相同。这不是第二版书籍,而是用于向读者解释基本概念。

web3.js 简介

web3.js 为我们提供了与 geth 通信的 JavaScript API。它在内部使用 JSON-RPC 与 geth 通信。web3.js 也可以与支持 JSON-RPC 的任何其他类型的以太坊节点通信。它将所有 JSON-RPC API 公开为 JavaScript API。它不仅支持与以太坊相关的所有 API,还支持与 WhisperSwarm 相关的 API。

随着我们构建各种项目,你将继续学习更多关于 web3.js 的知识。不过,现在让我们先了解一些 web3.js 最常用的 API。稍后,我们将使用 web3.js 为我们在上一章中创建的所有权智能合约构建一个前端。

在撰写本文时,web3.js 的最新版本是 1.0.0-beta.18。我们将使用此版本学习所有内容。

web3.js 托管在 github.com/ethereum/web3.js,完整文档托管在 github.com/ethereum/wiki/wiki/JavaScript-API

导入 web3.js

只需在项目目录中运行 npm install web3 即可在 Node.js 中使用 web3.js。在源代码中,可以使用 require("web3"); 进行导入。

要在客户端 JavaScript 中使用 web3.js,可以将 web3.js 文件入队,该文件位于项目源代码的 dist 目录中。现在,web3 对象将全局可用。

连接到节点

web3.js 可以使用 HTTP 或 IPC 与节点通信,并允许我们连接多个节点。我们将使用 HTTP 进行节点通信。web3 的一个实例表示与节点的连接。该实例公开了 API。

当应用程序在 mist 中运行时,它会自动创建一个连接到 mist 节点的 web3 实例。实例的变量名为 web3

这是连接到节点的基本代码:

if (typeof web3 !== 'undefined') { 
  web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 
} 

首先,我们通过检查 web3 是否为 undefined 来验证代码是否在 mist 中运行。如果 web3 已定义,那么我们使用已有的实例;否则,我们通过连接到自定义节点来创建一个实例。如果你想无论应用是否在 mist 中运行都连接到自定义节点,可以从上述代码中移除 if 条件。在这里,我们假设我们的自定义节点在本地的 8545 端口上运行。

Web3.providers 对象暴露了构造函数(在这个上下文中称为providers),用于建立连接并使用各种协议传输消息。Web3.providers.HttpProvider 允许我们建立 HTTP 连接,而 Web3.providers.IpcProvider 允许我们建立 IPC 连接。

web3.currentProvider 属性会自动分配给当前的提供者实例。在创建一个 web3 实例后,你可以使用 web3.setProvider() 方法来更改它的提供者。它接受一个参数,即新提供者的实例。

请记住,默认情况下,geth 禁用了 HTTP-RPC。因此,在运行 geth 时通过传递 --rpc 选项来启用它。HTTP-RPC 默认运行在 8545 端口上。

web3 暴露了一个 isConnected() 方法,用于检查是否连接到节点。根据连接状态,它返回一个 truefalse 的值。

API 结构

web3 包含一个专门用于以太坊区块链交互的 eth 对象(web3.eth)以及一个用于 Whisper 交互的 shh 对象(web3.shh)。大部分 web3.js API 都在这两个对象内部。

所有 API 默认情况下都是同步的。对于异步请求,你可以为大多数函数的最后一个参数传递一个可选的回调。所有回调都使用错误优先的回调风格。

一些 API 对于异步请求有一个别名。例如,web3.eth.coinbase() 是同步的,而 web3.eth.getCoinbase() 是异步的。

这里有一个例子:

//sync request 
try 
{ 
  console.log(web3.eth.getBlock(48)); 
} 
catch(e) 
{ 
  console.log(e); 
} 

//async request 
web3.eth.getBlock(48, function(error, result){ 
    if(!error) 
        console.log(result) 
    else 
        console.error(error); 
}) 

getBlock 用于使用其编号或哈希获取块的信息。或者它可以接受字符串,例如 "earliest"(创世块)、"latest"(区块链的顶块)或 "pending"(正在挖矿的块)。如果不传递参数,则默认为 web3.eth.defaultBlock,默认分配为 "latest"

所有需要块标识作为输入的 API 默认情况下可以接受数字、哈希或可读字符串之一。如果未传递值,这些 API 默认使用 web3.eth.defaultBlock

BigNumber.js

JavaScript 在处理大数时表现很差。因此,对于需要处理大数并进行精确计算的应用程序,请使用 BigNumber.js 库。

web3.js 也依赖于 BigNumber.js,并自动添加它。web3.js 总是返回 BigNumber 对象作为数字值。它可以接受 JavaScript 数字、数字字符串和 BigNumber 实例作为输入。

让我们来演示一下,如下所示:

web3.eth.getBalance("0x27E829fB34d14f3384646F938165dfcD30cFfB7c")
  .toString(); 

在这里,我们使用 web3.eth.getBalance() 方法获取地址的余额。此方法返回一个 BigNumber 对象。我们需要对 BigNumber 对象调用 toString() 来将其转换为数字字符串。

BigNumber.js 无法正确处理具有超过 20 个浮点数字的数字。因此,建议您将余额存储在 wei 单位中,并在显示时将其转换为其他单位。web3.js 本身总是以 wei 单位返回和接受余额。例如,getBalance() 方法返回以 wei 为单位的地址余额。

单位转换

web3.js 提供了将 wei 余额转换为任何其他单位以及反之的 API。

web3.fromWei() 方法将 wei 数字转换为另一个单位,而 web3.toWei() 方法将任何其他单位的数字转换为 wei。以下是一个示例来演示这一点:

web3.fromWei("1000000000000000000", "ether"); 
web3.toWei("0.000000000000000001", "ether"); 

在第一行中,我们将 wei 转换为 ether;在第二行中,我们将 ether 转换为 wei。这两个方法中的第二个参数可以是以下字符串之一:

  • kweiada

  • mweibabbage

  • gweishannon

  • szabo

  • finney

  • ether

  • kether / grand / einstein

  • mether

  • gether

  • tether

检索 gas 价格、余额和交易详细信息

让我们来看一下检索 gas 价格、地址余额以及已挖掘交易信息的 API:

//It's sync. For async use getGasPrice 
console.log(web3.eth.gasPrice.toString()); 

console.log(web3.eth.getBalance("0x407d73d8a49eeb85d32cf465507dd71d5071
  00c1", 45).toString()); 

console.log(web3.eth.getTransactionReceipt("0x9fc76417374aa880d4449a1f7
  f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")); 

输出将以此格式显示:

20000000000 
30000000000 
{ 
 "transactionHash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ", 
 "transactionIndex": 0, 
 "blockHash": "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46", 
 "blockNumber": 3, 
 "contractAddress": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", 
 "cumulativeGasUsed": 314159, 
 "gasUsed": 30234 
} 

以下是前述方法的工作原理:

  • web3.eth.gasPrice(): 通过 x 最新区块的中位数 gas 价格确定 gas 价格。

  • web3.eth.getBalance(): 返回给定地址的余额。所有的哈希都应该以十六进制字符串(而不是十六进制字面值)的形式提供给 web3.js API。对于 Solidity 地址类型的输入也应该是十六进制字符串。

  • web3.eth.getTransactionReceipt(): 用于使用其哈希获取有关交易的详细信息。如果在区块链中找到了交易,则返回一个交易收据对象;否则返回 null。交易收据对象包含以下属性:

    • blockHash: 交易所在区块的哈希。

    • blockNumber: 此交易所在的区块编号。

    • transactionHash: 交易的哈希。

    • transactionIndex: 交易在区块中的位置的整数。

    • from: 发送者的地址。

    • to: 接收方的地址;当它是一个合约创建交易时,这个参数被设为 null

    • cumulativeGasUsed: 在该交易在区块中执行时使用的总 gas 量。

    • gasUsed: 仅由此特定交易使用的 gas 量。

    • contractAddress: 如果交易是合约创建,则创建的合约地址。否则,这将被设为 null

    • logs: 此交易生成的日志对象数组。

发送 ether

让我们看看如何向任何地址发送ether。要发送ether,您需要使用web3.eth.sendTransaction()方法。此方法可用于发送任何类型的交易,但主要用于发送ether。这是因为使用此方法部署合约或调用合约的方法很麻烦,因为您需要手动生成交易数据而不是自动生成。它接受一个具有以下属性的交易对象:

  • from:发送账户的地址。如果未指定,则使用web3.eth.defaultAccount属性。

  • to:这是可选的。这是消息的目的地地址,对于合约创建交易则未定义。

  • value:这是可选的。交易的价值以 wei 为单位进行转移,如果是合约创建交易,则还包括捐赠金。

  • gas:这是可选的。这是用于交易的燃气量(未使用的燃气将退还)。如果未提供,则会自动确定。

  • gasPrice:这是可选的。这是交易的燃气价格,以 wei 为单位,默认为平均网络燃气价格。

  • data:这是可选的。它要么是包含消息相关数据的字节字符串,要么在合约创建交易的情况下是初始化代码。

  • nonce:这是可选的。这是一个整数。每个交易都与nonce相关联。nonce是一个计数器,表示发送者所做交易的数量。如果未提供,它将自动确定。它有助于防止重放攻击。此nonce不是与块相关联的nonce。如果我们使用的nonce大于交易应该具有的nonce,则该交易将放入队列,直到其他交易到达。例如,如果下一个交易的nonce应为四,而我们将nonce设置为十,则 geth 将等待其余的六个交易到达后再广播此交易。nonce为十的交易称为排队的交易,而不是待处理的交易。

下面是一个发送ether到地址的示例:

var txnHash = web3.eth.sendTransaction({ 
  from: web3.eth.accounts[0], 
  to: web3.eth.accounts[1], 
  value: web3.toWei("1", "ether") 
}); 

在这里,我们从账户编号0发送了一个ether到账户编号1。我们需要确保在运行 geth 时使用unlock选项来解锁两个账户。geth 交互式控制台会提示输入密码,但是在交互式控制台之外使用的 web3.js API 如果账户被锁定,将会抛出错误。此方法返回交易的事务哈希。然后,您可以使用getTransactionReceipt()方法检查交易是否已被挖掘。

你还可以使用web3.personal.listAccounts()web3.personal.unlockAccount(addr, pwd)web3.personal.newAccount(pwd) API 在运行时管理账户。

与合约工作

让我们学习如何部署新合同,使用其地址获取已部署合同的引用,向合同发送ether,发送交易以调用contract方法,并估算方法调用的 gas。

要部署新合同或获取对已部署合同的引用,您需要首先使用web3.eth.contract()方法创建一个contract对象。它以合同 ABI 作为参数,并返回contract对象。

这是创建contract对象的代码:

var proofContract = web3.eth.contract([{"constant":false,"inputs":
  [{"name":"fileHash","type":"string"}],"name":"get","outputs":
  [{"name":"timestamp","type":"uint256"},
  {"name":"owner","type":"string"}],"payable":false,"type":"function"},
  {"constant":false,"inputs":[{"name":"owner","type":"string"},
  {"name":"fileHash","type":"string"}],"name":"set","outputs":
  [],"payable":false,"type":"function"},{"anonymous":false,"inputs":
  [{"indexed":false,"name":"status","type":"bool"},
  {"indexed":false,"name":"timestamp","type":"uint256"},
  {"indexed":false,"name":"owner","type":"string"}, 
  {"indexed":false,"name":"fileHash","type":"string"}],"name"
  :"logFileAddedStatus","type":"event"}]);

一旦您获得了合同,您可以使用contract对象的new方法部署它,或者使用at方法获取与 ABI 匹配的已部署合同的引用。

让我们看一个部署新合同的示例,如下所示:

 var proof = proofContract.new({
         from: web3.eth.accounts[0],
         data: "0x606060405261068...",
         gas: "4700000"
     },
     function(e, contract) {
         if (e) {
             console.log("Error " + e);
         } else if (contract.address != undefined) {
             console.log("Contract Address: " + contract.address);
         } else {
             console.log("Txn Hash: " + contract.transactionHash)
         }
     })

这里,new方法是异步调用的,因此如果交易成功创建并广播,则回调将被调用两次。第一次,在交易广播后调用,第二次,在交易挖掘后调用。如果您不提供回调函数,则proof变量的address属性将设置为undefined。一旦contract被挖掘,address属性将被设置。

proof合同中,没有构造函数,但如果有构造函数,则构造函数的参数应该放在new方法的开头。我们传递的对象包含from地址,合同的字节码和要使用的最大gas。这三个属性必须存在才能创建交易。此对象可以具有传递给sendTransaction()方法的对象中存在的属性,但在这里,data是合同的字节码,to属性被忽略。

您可以使用at方法获取已部署合同的引用。以下是演示此操作的代码:

var proof = 
  proofContract.at("0xd45e541ca2622386cd820d1d3be74a86531c14a1");

现在让我们看一下发送交易以调用合同方法的情况。以下是演示此操作的示例:

proof.set.sendTransaction("Owner Name", 
  "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", { 

from: web3.eth.accounts[0], 
}, function(error, transactionHash){ 

if (!err) 

console.log(transactionHash); 
}) 

在这里,我们为同名方法调用对象的sendTransaction方法。传递给此sendTransaction方法的对象具有与web3.eth.sendTransaction()相同的属性,只是忽略了datato属性。

如果您想调用节点本身的方法,而不是创建交易并广播它,那么您可以使用sendTransaction而不是sendTransaction。如下所示:

var returnValue = proof.get.call
  ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

有时,有必要了解调用方法所需的 gas 量,以便您可以决定是否调用它。您可以使用web3.eth.estimateGas来实现此目的。然而,使用web3.eth.estimateGas()需要您直接生成交易的数据;因此,我们可以使用合同对象的estimateGas()方法。以下是演示此操作的示例:

var estimatedGas = proof.get.estimateGas
  ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

要向合同发送一些ether而不调用任何方法,您可以简单地使用web3.eth.sendTransaction方法。

检索和监听合约事件

监听事件非常重要,因为通常通过触发事件返回交易调用的方法结果。

在学习如何检索和监听事件之前,我们需要了解事件的索引参数。 事件的最多三个参数可以具有 indexed 属性。 此属性用于指示节点将其索引,以便应用客户端可以搜索具有匹配返回值的事件。 如果不使用 indexed 属性,则必须从节点检索所有事件并过滤所需的事件。 例如,您可以这样编写 logFileAddedStatus 事件:

event logFileAddedStatus(bool indexed status, uint indexed timestamp,
  string owner, string indexed fileHash); 

下面是一个示例,演示如何监听合约事件:

var event = proof.logFileAddedStatus(null, {
 fromBlock: 0,
 toBlock: "latest"
});
event.get(function(error, result) {
 if (!error) {
 console.log(result);
 } else {
 console.log(error);
 }
})
event.watch(function(error, result) {
 if (!error) {
 console.log(result.args.status);
 } else {
 console.log(error);
 }
})
setTimeout(function() {
 event.stopWatching();
}, 60000)
var events = proof.allEvents({
 fromBlock: 0,
 toBlock: "latest"
});
events.get(function(error, result) {
 if (!error) {
 console.log(result);
 } else {
 console.log(error);
 }
})
events.watch(function(error, result) {
 if (!error) {
 console.log(result.args.status);
 } else {
 console.log(error);
 }
})
setTimeout(function() {
 events.stopWatching();
}, 60000)

以下是上述代码的工作原理:

  • 首先,我们通过在合约实例上调用事件同名方法来获取 event 对象。 此方法接受两个对象作为参数,用于筛选事件:

    • 第一个对象用于通过索引返回值筛选事件,例如,{'valueA': 1, 'valueB': [myFirstAddress, mySecondAddress]}。 所有筛选值默认设置为 null。 这意味着它们将匹配来自此合约的给定类型的任何事件。

    • 下一个对象可以包含三个属性:fromBlock(“最早”的块;默认情况下为"latest");toBlock(“最新”的块;默认情况下为"latest");和 address(仅从中获取日志的地址列表;默认情况下为合约地址)。

  • event 对象公开三种方法:getwatchstopWatchingget 用于获取区块范围内的所有事件。 watch 类似于 get,但它在获取事件后监视更改。 stopWatching 可用于停止监视更改。

  • 然后,我们有了合约实例的 allEvents 方法。 它用于检索合约的所有事件。

每个事件都由一个对象表示,其中包含以下属性:

  • args:包含事件参数的对象。

  • event:表示事件名称的字符串。

  • logIndex:表示块中的日志索引位置的整数。

  • transactionIndex:表示创建此索引位置日志的事务的整数。

  • transactionHash:表示创建此日志的交易的哈希的字符串。

  • address:表示此日志来源地址的字符串。

  • blockHash:表示包含此日志的块的哈希的字符串。 当处于待定状态时,此字段为 null

  • blockNumber:此日志所在的块号。 当处于待定状态时,此字段为 null

web3.js 提供了一个web3.eth.filter API 来检索和监视事件。您可以使用这个 API,但是在前一种方法中处理事件的方式要简单得多。您可以在github.com/ethereum/wiki/wiki/JavaScript-API#web3ethfilter了解更多信息。

为所有权合约构建客户端

在上一章中,我们为所有权合约编写了 Solidity 代码。在上一章和本章中,我们学习了 web3.js 以及如何使用 web3.js 调用合约的方法。现在,是时候为我们的智能合约构建一个客户端,以便用户可以轻松地使用它。

我们将构建一个客户端,企业用户选择文件,输入所有者细节,然后点击提交来广播一个交易来调用合约的set方法,使用文件哈希和所有者细节。一旦成功广播交易,我们将显示交易哈希。用户还可以选择一个文件,并从智能合约获取所有者的细节。客户端还将实时显示最近的set交易。

我们将在前端使用 sha1.js 来获取文件哈希,使用 jQuery 进行 DOM 操作,并使用 Bootstrap 4 来创建响应式布局。我们将在后端使用 Express.js 和 web3.js。我们将使用socket.io,这样后端就可以将最近挖掘到的交易推送到前端,而无需前端周期性地请求数据。

项目结构

在本章的练习文件中,您会找到两个目录:FinalInitialFinal包含项目的最终源代码,而Initial包含空白源代码文件和库,以便您快速开始构建应用程序。

要测试Final目录,您需要在其中运行npm install,并将app.js中的硬编码合约地址替换为部署合约后获得的合约地址。然后,使用Final目录内的node app.js命令运行应用程序。

Initial目录中,您会找到一个public目录和两个名为app.jspackage.json的文件。package.json包含我们应用的后端依赖,app.js是您放置后端源代码的地方。

public目录包含与前端相关的文件。在public/css目录内,您会找到bootstrap.min.css,这是 Bootstrap 库;在public/html目录内,您会找到index.html,您将在其中放置应用程序的 HTML 代码;而在public/js目录内,您会找到用于 jQuery、sha1 和 socket.io 的 JS 文件。在public/js目录内,您还会找到一个main.js文件,您将在其中放置我们应用的前端 JS 代码。

构建后端

首先,在 Initial 目录内运行 npm install 安装我们后端所需的依赖项。在开始编写后端之前,请确保 geth 正在运行,并启用了 rpc。最后,请确保账户 0 存在并已解锁。

在开始编码之前,你需要做的最后一件事是使用我们在前一章中看到的代码部署所有权合同,并复制合同地址。

现在让我们创建一个单独的服务器,该服务器将为浏览器提供 HTML,并接受 socket.io 连接:

var express = require("express"); 
var app = express(); 
var server = require("http").createServer(app); 
var io = require("socket.io")(server); 
server.listen(8080); 

在这里,我们将 expresssocket.io 服务器整合到一个运行在端口 8080 上的服务器中。

现在让我们创建路由来提供静态文件并为应用程序的主页创建路由。以下是执行此操作的代码:

app.use(express.static("public")); 
app.get("/", function(req, res){ 
  res.sendFile(__dirname + "/public/html/index.html"); 
}) 

这里,我们使用 express.static 中间件来提供静态文件服务。我们要求它在 public 目录中查找静态文件。

现在让我们连接到 geth 节点,并获取已部署的合同的引用,以便我们可以发送交易并监听事件。以下是执行此操作的代码:

var Web3 = require("web3"); 

web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 

var proofContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"fileHash","type":"string"}],"name":"get","outputs":[{"name":"timestamp","type":"uint256"},{"name":"owner","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"string"},{"name":"fileHash","type":"string"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"bool"},{"indexed":false,"name":"timestamp","type":"uint256"},{"indexed":false,"name":"owner","type":"string"},{"indexed":false,"name":"fileHash","type":"string"}],"name":"logFileAddedStatus","type":"event"}]); 

var proof = 
  proofContract.at("0xf7f02f65d5cd874d180c3575cb8813a9e7736066"); 

代码很直观。只需用你得到的合同地址替换原来的地址即可。

现在让我们创建路由来广播交易并获取有关文件的信息。以下是执行此操作的代码:

app.get("/submit", function(req, res){
  var fileHash = req.query.hash;
  var owner = req.query.owner;
  var pkeys = req.query.pkeys;

  pkeys = pkeys.split(",")

  proof.set.sendTransaction(owner, fileHash, {
    from: web3.eth.accounts[0],
    privateFor: pkeys
  }, function(error, transactionHash){
    if (!error)
    {
      res.send(transactionHash);
    }
    else
    {
      res.send("Error");
    }
  })
})

app.get("/getInfo", function(req, res) {
    var fileHash = req.query.hash;
    var details = proof.get.call(fileHash);
    res.send(details);
})

这里,/submit 路由用于创建和广播交易。一旦我们获得了交易哈希,我们就将其发送给客户端。我们不会做任何等待交易挖矿的操作。/getInfo 路由调用节点上合同的 get 方法,而不是创建一个交易。它只是简单地将收到的任何响应发送回去。

现在让我们监听来自合同的事件,并将它们广播给所有客户端。以下是执行此操作的代码:

proof.logFileAddedStatus().watch(function(error, result) {
    if (!error) {
        if (result.args.status == true) {
            io.send(result);
        }
    }
})

在这里,我们检查 status 是否为 true,只有当它为 true 时,我们才将事件广播到所有连接的 socket.io 客户端。

构建前端

让我们从应用程序的 HTML 开始。将此代码放在 index.html 文件中,如下所示:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta name="viewport" content="width=device-width, initial-
          scale=1, shrink-to-fit=no"> 
        <link rel="stylesheet" href="/css/bootstrap.min.css"> 
    </head> 
    <body> 
        <div class="container"> 
            <div class="row"> 
                <div class="col-md-6 offset-md-3 text-xs-center"> 
                    <br> 
                    <h3>Upload any file</h3> 
                    <br> 
                    <div> 
                        <div class="form-group"> 
                            <label class="custom-file text-xs-left"> 
                                <input type="file" id="file" 
                                  class="custom-file-input"> 
                                <span class="custom-file-control">
                                </span> 
                            </label> 
                        </div> 
                        <div class="form-group"> 
                            <label for="owner">Enter owner name</label> 
                            <input type="text" class="form-control"
                              id="owner"> 
                        </div> 
                        <div class="form-group">
                            <label for="owner">Enter Public Keys
                            <small>(comma Seperated)</small></label>
                            <input type="text" class="form-control"
                             id="pkeys">
                        </div>
                        <button onclick="submit()" class="btn btn-
                         primary">Submit</button> 
                        <button onclick="getInfo()" class="btn btn-
                        primary">Get Info</button> 
                        <br><br> 
                        <div class="alert alert-info" role="alert" 
                         id="message"> 
                            You can either submit the file's details or
                             get information about it. 
                        </div> 
                    </div> 
                </div> 
            </div> 
            <div class="row"> 
                <div class="col-md-6 offset-md-3 text-xs-center"> 
                    <br> 
                    <h3>Live Transactions Mined</h3> 
                    <br> 
                    <ol id="events_list">No Transaction Found</ol> 
                </div> 
            </div> 
        </div> 
        <script type="text/javascript" src="img/sha1.min.js"></script> 
        <script type="text/javascript" src="img/jquery.min.js">
          </script> 
        <script type="text/javascript" src="img/socket.io.min.js">
          </script> 
        <script type="text/javascript" src="img/main.js"></script> 
    </body> 
</html> 

以下是代码的工作原理:

  • 首先,我们显示 Bootstrap 的文件输入字段,以便用户可以选择文件。

  • 然后,我们显示一个文本字段,用户可以输入所有者的详细信息。

  • 然后我们有两个按钮。第一个按钮用于在合同中存储文件哈希和所有者的详细信息,第二个按钮用于从合同中获取文件的信息。点击 Submit 按钮会触发 submit() 方法,点击 Get Info 按钮会触发 getInfo() 方法。

  • 接下来,我们有一个警告框来显示消息。

  • 最后,我们显示一个有序列表,以显示用户在页面上时挖矿的合同的交易。

现在,让我们编写 getInfo()submit() 方法的实现,建立与服务器的 socket.io 连接,并监听来自服务器的 socket.io 消息。将此代码放在 main.js 文件中:

function submit()
{
  var file = document.getElementById("file").files[0];

  if(file)
  {
    var owner = document.getElementById("owner").value;

    if(owner == "")
    {
      alert("Please enter owner name");
    }
    else
    {
      var publicKeys = document.getElementById("pkeys").value;

      if(publicKeys == "")
      {
        alert("Please enter the other enterprise's public keys");
      }
      else
      {
        var reader = new FileReader();
        reader.onload = function (event) {
            var hash = sha1(event.target.result);

            $.get("/submit?hash=" + hash + "&owner=" + owner + 
            "&pkeys=" + encodeURIComponent(publicKeys), function(data){
              if(data == "Error")
              {
                $("#message").text("An error occured.");
              }
              else
              {
                $("#message").html("Transaction hash: " + data);
              }
            });
        };
        reader.readAsArrayBuffer(file);
      }
    }
  }
  else
  {
    alert("Please select a file");
  }
}

function getInfo()
{
  var file = document.getElementById("file").files[0];

  if(file)
  {
    var reader = new FileReader();
    reader.onload = function (event) {
        var hash = sha1(event.target.result);

        $.get("/getInfo?hash=" + hash, function(data){
          if(data[0] == 0 && data[1] == "")
          {
            $("#message").html("File not found");
          }
          else
          {
            $("#message").html("Timestamp: " + data[0] + " Owner: " + 
              data[1]);
          }
        });
    };
    reader.readAsArrayBuffer(file);
  }
  else
  {
    alert("Please select a file");
  }
}

var socket = io("http://localhost:8080");

socket.on("connect", function () {
  socket.on("message", function (msg) {
    if($("#events_list").text() == "No Transaction Found")
    {
      $("#events_list").html("<li>Txn Hash: " + msg.transactionHash + 
        "\nOwner: " + msg.args.owner + "\nFile Hash: " +
           msg.args.fileHash + "</li>");
    }
    else
    {
      $("#events_list").prepend("<li>Txn Hash: " + msg.transactionHash 
        + "\nOwner: " + msg.args.owner + "\nFile Hash: " + 
          msg.args.fileHash + "</li>");
    }
    });
});

这是前述代码的工作原理:

  • 首先,我们定义了submit()方法。在submit()方法中,我们确保选择了一个文件并且文本字段不为空。然后,我们将文件内容读取为一个数组缓冲区,并将数组缓冲区传递给 sha1.js 中暴露的sha1()方法,以便获得数组缓冲区内的内容的哈希值。一旦我们获得了哈希值,我们就使用 jQuery 向/submit路由发出 AJAX 请求,然后在警告框中显示事务哈希值。

  • 紧接着我们定义了getInfo()方法。它首先确保选择了一个文件。然后,它生成像之前生成的那样的哈希,并请求/getInfo端点以获取有关该文件的信息。

  • 最后,我们使用socket.io库提供的io()方法建立socket.io连接。然后,我们等待连接事件触发,这表示连接已经建立。连接建立后,我们监听来自服务器的消息,并显示交易的详细信息给用户。

我们不把文件存储在以太坊区块链上。存储文件非常昂贵,因为需要很多 gas。在我们的情况下,我们不需要存储文件,因为网络中的节点将能够看到文件;因此,如果用户想保持文件内容的机密性,那么他们将不能。我们应用的目的仅仅是证明文件的所有权,而不是像云服务一样存储和提供文件。

测试客户端

现在运行app.js节点来运行应用程序服务器。打开你喜欢的浏览器,并访问http://localhost:8080/。你将在浏览器中看到这个输出:

现在选择一个文件,输入所有者的姓名,然后点击提交。浏览器窗口将会变成这样:

在下图中,你可以看到交易哈希值已显示。现在等待直到交易被挖掘。一旦交易被挖掘,你将能够在实时交易列表中看到交易。浏览器窗口应该如下所示:

现在再次选择相同的文件,然后点击获取信息按钮。你将会看到以下输出:

在上个截图中,你可以看到时间戳和所有者的详细信息。现在我们已经完成了为我们的第一个 DApp 构建客户端。

摘要

在本章中,我们首先学习了 web3.js 的基础知识,并查看了一些示例。我们了解了连接到节点、基本 API、发送各种类型的交易以及监听事件。最后,我们为我们的所有权合约构建了适当的生产用客户端。现在,您应该能够轻松编写智能合约并为其构建 UI 客户端,以便简化它们的使用。

在下一章中,我们将学习使用零知识安全层实现隐私。

第五章:建立互操作性区块链

有各种许可和公共区块链代表不同的资产、信息和业务流程。使用这些网络的主要需求之一是使它们能够彼此通信,这是一个需要克服的主要挑战。如果只有一个区块链能够统治它们所有,那将是很棒的,但绝对不会发生,因为只有一个区块链无法在安全性、隐私性、效率性、灵活性、平台复杂性、开发人员易用性等方面取得胜利。未来的区块链将是数个公共和许可区块链之间的互操作。本章中,我们将探讨如何实现多个夸梦网络之间的互操作性。

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

  • 理解区块链互操作性及相关的各种流行项目

  • 研究互操作性区块链可以实现的用例

  • 鉴于可以用于实现区块链互操作性的各种技术和模式

  • 建立代表联邦硬币的互操作性区块链网络

理解区块链互操作性

互操作性的区块链是可以彼此通信的区块链。每个区块链可以读取另一个区块链的状态。很多时候,你可能想要启用你的智能合约与中心化或其他去中心化的应用交互,当我们谈论 DApp 之间的互操作性时,我们在谈论与区块链之间的互操作性有所不同。

使以太坊智能合约能够检查文件是否存在于 IPFS 中,这就实现了 DApp 之间的互操作性,而使以太坊智能合约能够获取比特币账户余额则实现了区块链之间的互操作性。

让以太坊智能合约调用 REST API 以调用中心化应用被认为是在以太坊和 WWW 之间实现互操作性。但在本章中,我们将了解区块链之间的互操作性,特别是夸梦网络之间的互操作性。

目前正在开发的各种流行项目旨在采用去中心化机制实现区块链之间的互操作性。各种流行项目,如CosmosPolkadotInterledgerBlock Collider等,正在积极开发,以使区块链互操作化去中心化和易于实现,但在本章中我们不会涵盖这些项目,因为它们的目标是将互操作性带给公共区块链。然而,我们将学习创建互操作性区块链所使用的策略,正在被这些项目使用。

互操作性的区块链可以实现什么?

在进一步研究如何实现区块链互操作性之前,我们需要知道互操作性区块链可以实现哪些事情。显然,互操作性区块链有许多用例,但我们将重点关注互操作性区块链主要旨在实现的用例。它可以实现以下一个或多个用例:

  • 可移植资产:在不同区块链之间来回转移资产。这也被称为一对一挂钩双向挂钩

  • 支付对支付和支付对交付:技术上称为原子交换。当两个用户交换存在于两个不同区块链中的资产时,需要一个保证,即要么两个转账都发生,要么都不发生。例如,如果一个区块链持有数字化美元,另一个区块链持有数字化欧元,那么用户应该能够原子地交换这些资产。

  • 获取信息并对事件做出反应:一个区块链能够读取另一个区块链上存在的信息,或者对另一个区块链上发生的交易做出反应。例如,一个区块链代表租赁合同,另一个代表锁定的安全押金,因此当第一个区块链中的租赁合同到期时,第二个区块链应自动释放安全押金。

实现区块链互操作性的策略

让我们看看您可以实现前述一个或多个用例的各种策略。我们将学习可以在 Quorum 中实施的策略,但不是所有公共或其他区块链都可用的策略。我们还将看一些如何实现这些策略的示例。

单一托管者

实现互操作性的最简单方法是通过一个集中式第三方,使区块链能够相互通信。基本上,您需要信任这个第三方。

然而,像Oraclize这样的集中式互操作性项目已解决了信任问题。Oraclize 使您的以太坊智能合约能够与万维网进行通信;也就是说,它使您能够进行 REST API 调用,获取比特币账户余额,检查 IPFS 中的文件状态等等。Oraclize 还为智能合约提供了证明,证明结果没有被篡改;因此,Oraclize 解决了信任问题,但单点故障仍然存在。Oraclize 适用于权限和公共以太坊。Oraclize 的主要成就是为以太坊智能合约提供了进行 REST API 调用的能力,但它的目的从未是提供区块链之间的互操作性,因此它不提供此功能。

如果信任对你不是问题的话,那么单一托管策略绝对是你应该考虑的一件事,因为这支持我们先前讨论的所有三种用例。 您可以轻松编写一个集中式应用程序,以响应一个区块链上的事件并在另一个区块链上调用操作。 在得到许可的区块链中,通常有监管机构或权威机构可以选择承载该集中式应用程序。 单一托管方应发送使用预先编程为信任的区块链签名的交易。

多重签名联盟

在区块链之间实现互操作性的更好选择是由一组公证人(或权威)控制多重签名,其中大多数公证人必须批准某项操作才能进行。 这种设置比拥有单一托管方更好,但仍然集中控制。 要实现真正的去中心化,应谨慎选择公证人,至少具有以下特性:

  • 公证人的数量不应低—例如,至少 10 个。

  • 公证人的数量不应太高—例如,少于 30 个,这样用户可以验证公证人的真实性和诚实度。

  • 公证人应分布在不同的法律司法管辖区和国家,以防止国家攻击、胁迫和审查。

  • 为了防止自然灾害发生时基础设施的故障,公证人应该在地理上分布。

  • 公证人应是有名望的。

  • 公证人不应受(或依赖于)较低数量的实体控制。 例如,公证人不能是同一银行的不同分支。

  • 公证人应通过物理和逻辑保护以及所需的安全程序实现和保持指定级别的安全性。

侧链或中继

侧链是一种在一个区块链内部的系统,可以验证和读取其他区块链中的事件和/或状态。 中继是一种更直接的促进互操作性的方法,而不是依赖于可信中介向另一个区块链提供信息,区块链有效地承担了这一任务。 目前,使用Merkle 树来实现侧链系统。

通用的方法如下。假设在区块链 B 上执行的智能合约想要了解区块链 A 上是否发生了某个特定事件,或者在区块链 A 的状态中的某个特定对象在某个特定时间是否包含了某个值。我们可以在区块链 B 上创建一个合约,该合约接受区块链 A 的其中一个区块头,并使用区块链 A 共识算法的标准验证过程来验证这个区块头——在 IBFT 中,这将涉及验证超过 75% 的验证者签名已经签署了区块头。一旦中继验证了区块头已经被确定,中继就可以验证任何所需的交易或账户/状态条目,通过对梅克尔树的单个分支进行区块头验证。

所谓的 轻客户端验证 技术的应用对中继而言是理想的,因为区块链在资源上基本上是受限的。事实上,在同一时间,内部机制无法完全验证区块链 A 并使内部机制完全验证区块链 B,因为数学上简单的原因是两个盒子无法同时包含彼此:A 需要重新运行重新运行 A 的那部分部分,包括重新运行 B 的那部分 A,等等。然而,通过轻客户端验证,一个协议,其中区块链 A 包含区块链 B 的小片段,并且区块链 B 包含区块链 A 的小片段,这些片段是按需拉取的,是完全可行的。在区块链 B 上的一个中继的智能合约想要验证区块链 A 上的特定交易、事件或状态信息时,就像传统的轻客户端一样,会验证区块链 A 的加密哈希树的一个分支,然后通过区块头验证这个分支的根是否在内部,如果这两个检查通过,它会接受该交易、事件或状态信息是正确的。

注意,由于区块链是完全自包含的环境,并且没有自然访问外部世界的能力,因此链 A 的相关数据需要由用户输入到链 B 中;但是,因为数据在密码学意义上是自验证的,输入这些信息的用户无需受信任。

哈希锁定

哈希锁定是一种实现资产的原子交换的技术。它不需要任何中介。哈希锁定的工作原理如下:

  • 假设在两个不同的区块链中有两个名为 AB 的资产。资产 A 的所有者是 X,资产 B 的所有者是 Y

  • 如果他们两个都想交换这些资产,那么首先,X 必须生成一个秘密值 S,并计算秘密值的哈希 H。之后,XY 共享 H

  • 现在,X 锁定资产 A,声明如果 YN 秒内揭示 HS,则资产的所有权将转移到 Y;否则,资产将在 N 秒后解锁。

  • 接下来,Y 锁定资产 B,声明如果 XN/2 秒内揭示 HS,则资产的所有权将转移给 X;否则,资产将在 N/2 秒后解锁。

  • 所以现在,资产 AB 在分别 N 秒和 N/2 秒后被锁定。现在,在 N/2 秒内,XB 的区块链揭示 S 以主张资产的所有权。现在,Y 有同等的时间来了解 S 并向区块链 A 揭示 S 以主张所有权。

X 得到与 Y 给予的时间的一半的原因是只有 X 知道密码,而 Y 锁定资金后,X 可以等到 N 秒快结束时主张资产,这将不给 Y 足够的时间来主张他们的资金。因此,X 可以成功地窃取资产 AB。为了避免这种情况,我们给予 Y N 秒和 X N/2 秒,以便 Y 有与 X 相同的时间来主张资产。

这种技术的缺点是,如果 XN/2 秒和 N 秒之间将 S 揭示给区块链 B,那么 X 将无法主张对 B 的所有权,但 Y 将了解 S 并有时间主张对 A 的所有权。然而,这将是 X 的错,可以避免。

创建 FedCoin

FedCoin 是由中央银行发行的数字货币,与其法定货币一比一进行对冲。使用区块链数字化法定货币有几个好处,例如可以实现便捷的跨境支付,节省了对账工作等。

让我们在两个不同的区块链网络上构建一些数字化的印度卢比和美元。然后,让我们创建一些原子交换合约,以实现这些货币在银行之间的原子交换。这个用例需要您创建两个不同的 Quorum 网络,使用 IBFT 共识。在每个网络中有一个权威,即中央银行,以及 N 个同行,即其他银行。因此,您可以假设在第一个网络中,美联储系统FRS)是权威,美国银行BOA)和 ICICI 银行是同行。同样,在第二个网络中,印度储备银行RBI)是权威,BOA 和 ICICI 银行是同行。

您现在不必构建此网络,因为在构建和测试智能合约时,您只能使用具有四个以太坊账户地址的一个节点。这足以模拟整个场景。

用于数字化法定货币的智能合约

这里是一个基本的智能合约,用于在区块链上创建数字化美元。这个智能合约允许我们发行和转移数字化货币:

pragma solidity ⁰.4.19;

contract USD {

    mapping (address => uint) balances;
    mapping (address => mapping (address => uint)) allowed;
    address owner;

    function USD() {
        owner = msg.sender;
    }

    function issueUSD(address to, uint amount) {
        if(msg.sender == owner) {
            balances[to] += amount;
        } 
    }

    function transferUSD(address to, uint amount) {
        if(balances[msg.sender] >= amount) {
            balances[msg.sender] -= amount;
            balances[to] += amount;
        }
    }

    function getUSDBalance(address account) view returns 
      (uint balance) {
        return balances[account];
    }

    function approve(address spender, uint amount) {
        allowed[spender][msg.sender] = amount;
    }

    function transferUSDFrom(address from, address to, uint amount) {
        if(allowed[msg.sender][from] >= amount && balances[from]
          >= amount) {
            allowed[msg.sender][from] -= amount;
            balances[from] -= amount;
            balances[to] += amount;
        }
    }
}

这是上述代码的工作原理:

  • 首先,我们定义了一个映射来存储每家银行持有的美元数量。每家银行可以有多个地址以实现隐私。这些地址不一定是银行;它们也可以是其他智能合约,因为每个智能合约也有address

  • 接下来,我们假设中央银行部署了合约。因此,我们将中央银行定义为发行方,通过将其address分配给owner

  • 然后,我们定义了一个名为issueUSD的函数,中央银行可以利用它向其他银行发行美元。

  • 然后,我们定义了另一个名为transferUSD的函数,银行可以利用它在彼此之间转移美元。

  • 接下来,我们有一个函数用于读取账户的余额。

  • 最后,我们有两个重要的函数:approvetransferUSDFromtransferUSDFrom函数允许合约代表您发送美元。换句话说,您为同一区块链上的其他智能合约提供了管理您资金的 API。approve函数用于为智能合约提供管理您资金的批准。在调用approve时,您指定了该合约可以管理多少您的资金。

在这里,我们正在使用一个名为view的内置修饰符。view表示该函数无法修改存储,但将读取存储(因此查看)。view函数无法发送或接收以太币。类似地,还有另一个名为pure的修饰符,表示返回值只能依赖于输入参数,即它们甚至不能读取存储,也不能发送或接收以太币。您应该使用这些修饰符,因为它们具有多种好处——例如,在生成用于与合约交互的 UI 表单时,Remix IDE 会寻找这些修饰符。

现在在第二个网络中部署一个类似的合约以数字化印度卢比。在前述合约中用INR替换USD并部署它。应该是这样的:

pragma solidity ⁰.4.19;

contract INR {

    mapping (address => uint) balances;
    mapping (address => mapping (address => uint)) allowed;
    address owner;

    function INR() {
        owner = msg.sender;
    }

    function issueINR(address to, uint amount) {
        if(msg.sender == owner) {
            balances[to] += amount;
        } 
    }

    function transferINR(address to, uint amount) {
        if(balances[msg.sender] >= amount) {
            balances[msg.sender] -= amount;
            balances[to] += amount;
        }
    }

    function getINRBalance(address account) view returns 
      (uint balance) {
        return balances[account];
    }

    function approve(address spender, uint amount) {
        allowed[spender][msg.sender] = amount;
    }

    function transferINRFrom(address from, address to, uint amount) {
        if(allowed[msg.sender][from] >= amount && balances[from] 
          >= amount) {
            allowed[msg.sender][from] -= amount;
            balances[from] -= amount;
            balances[to] += amount;
        }
    }
}

原子交换智能合约

我们已成功数字化了法定货币。现在是实现哈希锁定机制的原子交换智能合约的时候了。我们在每个区块链上都有一个原子交换智能合约部署——也就是说,第一个区块链上的原子交换智能合约将美元锁定一段时间,并期望印度银行(这里是 ICICI 银行)在规定的时间内使用密钥来认领它。同样,第二个区块链上的原子交换合约将在规定的时间内锁定印度卢比,并期望美国银行(这里是 BOA)在规定的时间内使用密钥来认领它。

以下是用于锁定美元的原子交换智能合约:

pragma solidity ⁰.4.19;

import "./USD.sol";

contract AtomicSwap_USD {

    struct AtomicTxn {
        address from;
        address to;
        uint lockPeriod;
        uint amount;
    }

    mapping (bytes32 => AtomicTxn) txns;
    USD USDContract;

    event usdLocked(address to, bytes32 hash, uint expiryTime, 
      uint amount);
    event usdUnlocked(bytes32 hash);
    event usdClaimed(string secret, address from, bytes32 hash);

    function AtomicSwap_USD(address usdContractAddress) {
        USDContract = USD(usdContractAddress); 
    }

    function lock(address to, bytes32 hash, uint lockExpiryMinutes,
      uint amount) {
        USDContract.transferUSDFrom(msg.sender, address(this), amount);
        txns[hash] = AtomicTxn(msg.sender, to, block.timestamp + 
         (lockExpiryMinutes * 60), amount);
        usdLocked(to, hash, block.timestamp + (lockExpiryMinutes * 60),
          amount);
    }

    function unlock(bytes32 hash) {
        if(txns[hash].lockPeriod < block.timestamp) {
            USDContract.transferUSD(txns[hash].from, 
              txns[hash].amount);
            usdUnlocked(hash);
        }
    }

    function claim(string secret) {
        bytes32 hash = sha256(secret);
        USDContract.transferUSD(txns[hash].to, txns[hash].amount);
        usdClaimed(secret, txns[hash].from, hash);
    }

    function calculateHash(string secret) returns (bytes32 result) {
        return sha256(secret);
    }
}

以下是前述智能合约的工作原理:

  • 部署智能合约时,我们提供了USD合约的合约地址,以便它调用其函数来转移资金。

  • lock方法用于使用hash锁定资金。显然,在调用lock方法之前,BOA 必须批准此原子互换合约地址,以便能够访问其一定数量的资金。它接受hash并锁定资金一段时间。 amount被指定为指示锁定多少美元,此金额应小于等于批准的金额。 to地址指定印度银行的地址—即 ICICI 银行。因此,当 ICICI 银行来索取资金时,它们将去到此地址。该函数实际上将资金转移到其合约地址(即address(this))并触发事件,以便 ICICI 银行可以看到资金已被锁定。

  • unlock方法可以被 BOA 用来在hash过期后解锁资金,如果资金没有被索取。

  • claim方法由 ICICI 银行使用秘密索取资金。

  • 最后,我们使用calculateHash方法来计算秘密的hash

这里,我们以 BOA 和 ICICI 为例来简单解释,但之前的智能合约可以与任意数量的货币和银行配合良好运行。

在前述合同中将USD更改为INR,以为第二个区块链提供原子交换智能合约。以下是代码的外观:

pragma solidity ⁰.4.19;

import "./INR.sol";

contract AtomicSwap_INR {

    struct AtomicTxn {
        address from;
        address to;
        uint lockPeriod;
        uint amount;
    }

    mapping (bytes32 => AtomicTxn) txns;
    INR INRContract;

    event inrLocked(address to, bytes32 hash, uint expiryTime,
      uint amount);
    event inrUnlocked(bytes32 hash);
    event inrClaimed(string secret, address from, bytes32 hash);

    function AtomicSwap_INR(address inrContractAddress) {
        INRContract = INR(inrContractAddress); 
    }

    function lock(address to, bytes32 hash, uint lockExpiryMinutes, 
      uint amount) {
        INRContract.transferINRFrom(msg.sender, address(this), amount);
        txns[hash] = AtomicTxn(msg.sender, to, block.timestamp + 
         (lockExpiryMinutes * 60), amount);
        inrLocked(to, hash, block.timestamp + (lockExpiryMinutes * 60), 
          amount);
    }

    function unlock(bytes32 hash) {
        if(txns[hash].lockPeriod < block.timestamp) {
            INRContract.transferINR(txns[hash].from, 
              txns[hash].amount);
            inrUnlocked(hash);
        }
    }

    function claim(string secret) {
        bytes32 hash = sha256(secret);
        INRContract.transferINR(txns[hash].to, txns[hash].amount);
        inrClaimed(secret, txns[hash].from, hash);
    }

    function calculateHash(string secret) returns (bytes32 result) {
        return sha256(secret);
    }
}

测试

现在我们已经准备好在两个不同区块链的资产之间进行原子交换的智能合约了。接下来,让我们编写一些 JavaScript 代码来测试前面的合约并进行原子交换。以下代码允许您执行此操作。为了测试和模拟目的,您可以在一个单独的 Quorum 节点上运行以下代码,该节点具有四个帐户:

var generateSecret = function () {
    return Math.random().toString(36).substr(2, 9);
};

var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

var RBI_Address = "0x92764a01c43ca175c0d2de145947d6387205c655";
var FRS_Address = "0xbc37e7ba9f099ba8c61532c6fce157072798fe77";
var BOA_Address = "0x104803ea6d8696afa6e7a284a46a1e71553fcf12";
var ICICI_Address = "0x84d2dab0d783dd84c40d04692e303b19fa49bf47";

var usdContract_ABI = /* Put JSON here */;
var usdContract_Bytecode = "0x606..."
var atomicswapUSD_ABI = /* Put JSON here */;
var atomicswapUSD_Bytecode = "0x606..."
var inrContract_ABI = /* Put JSON here */;
var inrContract_Bytecode = "0x606..."
var atomicswapINR_ABI = /* Put JSON here */;
var atomicswapINR_Bytecode = "0x606..."

var usdContract = web3.eth.contract(usdContract_ABI);
var usd = usdContract.new({
  from: FRS_Address, 
   data: usdContract_Bytecode, 
   gas: "4700000"
}, function (e, contract){
  if (typeof contract.address !== 'undefined') {
    var usdContractAddress = contract.address;
    var usdContractInstance = usdContract.at(usdContractAddress)
    var atomicswap_usdContract = web3.eth.contract(atomicswapUSD_ABI);
    var atomicswap_usd = atomicswap_usdContract.new(usdContractAddress, {
        from: FRS_Address, 
        data: atomicswapUSD_Bytecode, 
        gas: "4700000"
    }, function (e, contract){
        if (typeof contract.address !== 'undefined') {
            var atomicSwapUSDAddress = contract.address;
            var atomicSwapUSDContractInstance =
              atomicswap_usdContract.at(atomicSwapUSDAddress);

            var inrContract = web3.eth.contract(inrContract_ABI);

        var inr = inrContract.new({
            from: RBI_Address, 
            data: inrContract_Bytecode, 
            gas: "4700000"
        }, function (e, contract){
            if(typeof contract.address !== 'undefined') {
                var inrContractAddress = contract.address;
                var inrContractInstance = 
                  inrContract.at(inrContractAddress)
            var atomicswap_inrContract =
              web3.eth.contract(atomicswapINR_ABI);
            var atomicswap_inr = atomicswap_inrContract.new(
                inrContractAddress, {
                from: RBI_Address, 
                data: atomicswapINR_Bytecode, 
                gas: '4700000'
            }, function (e, contract){
                if (typeof contract.address !== 'undefined') {
                    var atomicSwapINRAddress = contract.address;
                    var atomicSwapINRContractInstance = 
                      atomicswap_inrContract.at(atomicSwapINRAddress);

                }
            })
            }
        })
        }
    })
  }
})

首先,我们部署了USD合约,然后通过将USD合约的地址作为参数来部署了 USD 的原子交换合约。我们将这些合约部署为 FRS。然后,我们部署了INR合约,然后通过将INR合约的地址作为参数来部署了 INR 的原子交换合约。我们将这些合约部署为 RBI。

将以下代码放在提到的连续位置:

//Issue USD
usdContractInstance.issueUSD.sendTransaction(BOA_Address, 1000,
  {from: FRS_Address}, function(e, txnHash){

  //Fetch USD Balance
  console.log("Bank of America's USD Balance is : " + 
    usdContractInstance.getUSDBalance.call(BOA_Address).toString())

  //Issue INR
  inrContractInstance.issueINR.sendTransaction(ICICI_Address, 1000,
   {from: RBI_Address}, function(e, txnHash){

    //Fetch INR Balance
    console.log("ICICI Bank's INR Balance is : " + 
      inrContractInstance.getINRBalance.call(ICICI_Address).toString())

    //Generate Secret and Hash
    var secret = generateSecret();
    var hash = atomicSwapUSDContractInstance.calculateHash.call(secret,
      {from: BOA_Address});

    //Give Access to Smart Contract
    usdContractInstance.approve.sendTransaction(atomicSwapUSDAddress,
      1000, {from: BOA_Address}, function(e, txnHash){

      //Give Access to Smart Contract
      inrContractInstance.approve.sendTransaction(atomicSwapINRAddress,
        1000, {from: ICICI_Address}, function(e, txnHash){

        //Lock 1000 USD for 30 min
        atomicSwapUSDContractInstance.lock.sendTransaction(ICICI_Address, hash, 
  30, 1000, {from: BOA_Address, gas: 4712388}, function(e, txnHash){

          //Fetch USD Balance
          console.log("USD Atomic Exchange Smart Contracts holds : " + 
            usdContractInstance.getUSDBalance.call
            (atomicSwapUSDAddress).toString())

          //Lock 1000 INR for 15 min
          atomicSwapINRContractInstance.lock.sendTransaction(BOA_Address,
  hash, 15, 1000, {from: ICICI_Address, gas: 4712388},
  function(e, txnHash){

            //Fetch INR Balance
            console.log("INR Atomic Exchange Smart Contracts holds : "
              + inrContractInstance.getINRBalance.call
              (atomicSwapINRAddress).toString())

            atomicSwapINRContractInstance.claim(secret, {
              from: BOA_Address, gas: 4712388
            }, function(error, txnHash){

              //Fetch INR Balance
              console.log("Bank of America's INR Balance is : " +
                inrContractInstance.getINRBalance.call
                (BOA_Address).toString())

              atomicSwapUSDContractInstance.claim(secret, {
                from: ICICI_Address, gas: 4712388
              }, function(error, txnHash){

                //Fetch USD Balance
                console.log("ICICI Bank's USD Balance is : " +
                  usdContractInstance.getUSDBalance.call
                  (ICICI_Address).toString())
              })

            })

          })
        })
      })

    })
  })

}) 

以下是前面的代码如何运行的:

  1. 这里,FRS 向 BOA 发行了 USD,然后 RBI 向 ICICI 银行发行了 INR。

  2. 然后,BOA 生成了一个秘密。我们使用一个非常基本的函数来生成一个秘密。显然,在实际场景中,您应该使用某种基于硬件的工具来生成这些类型的安全秘密。

  3. 接下来,我们计算了秘密的哈希。

  4. BOA 和 ICICI 银行分别授予了 USD 原子交换和 INR 原子交换合约对其资金的访问权限。

  5. BOA 将 USD 锁定在 USD 原子交换合约中,锁定时间为 30 分钟,并声明只有 ICICI 银行可以索取资金。

  6. 同样,ICICI 银行将 INR 锁定在 INR 原子交换合约中,锁定时间为 15 分钟,并声明只有 BOA 可以索取资金。

  7. 最后,BOA 前去索取 INR。一旦 ICICI 知道了秘密,它立即索取了 USD。

要测试上述合约,首先复制你的以太坊地址,并替换我在之前示例中生成的地址。然后确保在你的节点上解锁所有四个账户。最后,编译合约并填充ABIBytecode变量。

这里,我们使用 Solidity 函数来计算hash,但你也可以使用 JavaScript 来计算hash。如果你想计算sha256哈希值,那么你可以使用任何一个 JavaScript 库,但如果你想像 Solidity 在 JavaScript 中计算sha3(也就是keccak256)一样,那么你需要使用web3.utils库,它提供了一个名为soliditySha3的函数。这个函数会以与 Solidity 相同的方式计算给定输入参数的sha3。这意味着参数将被ABI转换和紧密打包后再进行哈希运算。

摘要

在本章中,我们探讨了构建可互操作的区块链的各种选项。总结起来,单一托管方、多签名联邦以及哈希锁定易于实现,而侧链则复杂且需要大量工程工作。很快,我们将拥有内置侧链支持的生产许可区块链平台。

最后,我们通过模拟两家中央和商业银行来实现了哈希锁定。你可以继续尝试构建两个不同的网络,并尝试进行原子交换。

在下一章中,我们将学习如何构建一个用于 Quorum 的区块链服务器。在构建过程中,我们还将学习 DevOps 和云计算的概念。

第六章:构建 Quorum 作为服务平台

随着使用Kubernetes(K8s)部署容器化应用程序的增长,现在是学习如何将 Quorum 容器化以部署到 K8s 的正确时机。 在本章中,我们将构建一个平台即服务PaaS)以便轻松创建 Quorum 网络。 我们将从云计算、Docker 和 K8s 的基础知识开始,并最终建立一个Quorum 即服务QaaS)平台。 在本章中,我们将构建一个极简的区块链即服务BaaS),与 Azure、AWS 和 BlockCluster 等各种云平台提供的服务相比。

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

什么是云计算?

  • 公共、私有和混合云之间的区别

  • IaaS、PaaS 和 SaaS 之间的区别

  • 什么是 Docker 和应用程序的容器化?

  • 微服务架构简介

  • 了解 K8s 的基本原理及其优势

  • 在本地计算机上安装 minikube

  • 在 K8s 中部署一个简单的 Hello World Node.js 应用程序

  • 将 Quorum 容器化为 K8s

  • 使用 Docker 和 K8s 构建 QaaS 平台

云计算简介

简单来说,云计算是通过互联网提供计算服务(服务器、存储、数据库、网络、软件等)的按需交付。

云计算提供了更容易访问服务器、存储、数据库和广泛的应用服务的途径,这些服务都可以通过互联网获得。 云服务平台,如亚马逊网络服务微软 Azure,拥有并维护这些应用服务所需的网络连接硬件,而您则通过 Web 应用程序进行配置和使用所需的资源。

以下是云计算的优势:

  • 成本:云计算节省了很多成本,因为您不必购买硬件和软件。 它还节省了您在现场数据中心的设置和运行成本。 即使您设置了自己的数据中心,您也需要能够管理它们的 IT 专家,以及全天候的电力和冷却,这会增加额外的成本。 相比之下,云计算非常便宜。 在云计算中,您只有在使用资源时才需要付费,并且您只需要支付您使用的数量。

  • 速度:云计算可以节省时间,因为您可以在需要时立即运行服务; 它提供按需提供计算服务。

  • 全球扩展:您可以轻松地在多个地区部署您的应用程序。 这让您的应用程序靠近用户。

还有其他各种好处,取决于您使用的云计算提供商。

私有与公共与混合云

云解决方案可以是私有的、公共的或混合的,根据数据中心的所有权和位置。 云解决方案通常是公共的,也就是说,任何有互联网访问权限的人都可以使用云提供的计算服务。 我们之前看到的所有好处都是公共云提供的好处。

尽管公共云在配置计算服务时允许您选择您的区域,但可用区域的总数仍然非常有限。这是银行、军队和政府等实体的一个关注点,因为它们要么不希望数据离开他们的国家,要么不希望云提供商能够看到数据。因此,这些实体要么选择私有云,要么选择混合云。

当云托管在企业自己的数据中心时,称为私有云。在这种情况下,企业无法享受成本和多区域扩展的好处,因为它们负责配置和维护数据中心。

混合云术语用于企业根据技术和业务需求使用私有云和公共云的混合组合。企业可能选择将应用程序托管在公共云上,同时由于合规性或安全问题,将与应用程序相关的一些数据保留在私有云中。

IaaS 与 PaaS 和 SaaS 之间的区别

基础设施即服务IaaS)、平台即服务PaaS)和软件即服务SaaS)是基于您管理的内容以及云提供商为您管理的内容而划分的三种不同类别的云解决方案。

在 IaaS 中,云提供商为客户提供按需访问基本计算服务,即存储、网络和服务器。其他所有事项都由您来配置和管理。Amazon AWS、Google Cloud、Azure、Linode 和 Rackspace 是 IaaS 的示例。

在 PaaS 中,云提供商管理操作系统、编程语言的运行时、数据库和 Web 服务器——也就是为开发、测试和管理应用程序提供环境。简单来说,你只需要担心编写代码和业务方面的可扩展性。应用程序开发和部署的其余基础设施由云提供商处理。Heroku、Redhat 的 OpenShift、Apache Stratos 和 Google App Engine 是 PaaS 的示例。

数据库即服务DBaaSBaaS)属于 PaaS 类别。因此,在本章中,我们将创建一个简单的 PaaS:QaaS。任何管理应用程序依赖的服务(如数据库、区块链或消息队列)的云解决方案都是 PaaS。

在 SaaS 中,云提供商管理一切,包括数据和应用程序。您不需要编写任何代码来构建应用程序。云提供商提供一个界面,根据您的需求定制应用程序并部署它。使用 SaaS 往往会通过消除技术人员定期管理、编写代码和升级软件的需求来降低软件拥有成本。您只需担心业务逻辑。Salesforce、Google Apps 和 WordPress.com 是 SaaS 的示例。

上述图像可用于轻松确定云解决方案是 IaaS、PaaS 还是 SaaS。

一些云解决方案提供了 IaaS 和 PaaS 的功能。例如,AWS 最初是一个 IaaS,现在它还提供了各种按需服务(如区块链和弹性搜索)。

什么是容器?

如果你正在使用 PaaS 或 SaaS 来创建你的应用程序,那么你不会遇到容器,因为它们会负责容器化你的应用程序。PaaS 只是让你将应用程序的源代码推送到云端,并为你构建和运行应用程序。

如果你正在使用 IaaS 来构建你的应用程序,那么如果不将你的应用程序容器化,要扩展和管理你的应用程序将变得几乎不可能。让我们来看一个场景,并试着理解为什么我们需要容器。

在 IaaS 中,要部署你的应用程序,你需要执行以下步骤:

配置虚拟机(Virtual MachinesVMs

  1. 安装应用程序的所有依赖项和运行时环境

  2. 运行应用程序

  3. 如果应用程序开始接收的流量超过 VM 能够处理的范围,你将会开始创建新的 VM,并使用负载均衡器分发流量

  4. 对于每个新的 VM,你需要在运行新的应用程序实例之前按照相同的流程安装依赖项和运行时环境

这种滚动创建新的 VM 并在其中运行应用程序实例的过程容易出错且耗时。这就是容器发挥作用的地方。

简而言之,容器是一种打包应用程序的方式。容器的特殊之处在于当你将它们移动到新的机器或环境中时不会出现意外错误。你的应用程序的所有代码、库和依赖项都被打包在容器中作为一个不可变的工件。你可以将运行容器看作运行 VM,但不会带来启动整个操作系统的开销。因此,将应用程序打包在容器而不是 VM 中,将显著提高启动时间。容器比 VM 轻量级得多,使用的资源也少得多。

因此,对于上述示例,你需要为你的应用程序创建一个容器,并在每个 VM 中运行容器。显然,根据你的应用程序架构,一个 Docker 容器可以运行多个进程,一个 VM 可以运行多个容器。

在内部,PaaS 和 SaaS 使用容器来打包和部署你的应用程序。容器还有许多其他用途。例如:一个编码测试应用实际上在执行之前会将你的代码容器化,以便在隔离的环境中执行代码。

通过容器化应用程序及其依赖项,OS 分发和底层基础设施的差异被抽象化了。容器可以在裸机系统、云实例和 Linux、Windows 和 macOS 上的 VM 上运行。

Docker 简介

Docker 帮助您在容器内创建和部署软件。它是一套开源工具集,可帮助您构建、发布和运行任何应用程序。使用 Docker,您可以在应用程序源代码目录中创建一个特殊的文件,称为 Dockerfile。 Dockerfile 定义了一个构建过程,当输入到docker build 命令时,将生成一个不可变的 Docker 镜像。您可以将 Docker 镜像视为 VM 镜像。当您想要启动它时,只需使用 docker run 命令在 Docker 守护程序受支持并运行的任何地方运行它。Docker 容器是 Docker 镜像的运行实例。

在 Dockerfile 中,您需要提及一个应该运行的命令,然后容器启动。这就是容器内部执行实际应用程序的方式。如果命令存在,则容器也会关闭。当容器关闭时,所有写入容器卷中的数据都会丢失。

Docker 还提供了一个名为 Docker Hub 的基于云的仓库。您可以将其视为 Docker 镜像的 GitHub。您可以使用 Docker Hub 创建、存储和分发您构建的容器镜像。

构建一个 Hello World Docker 容器

让我们创建一个 Docker 镜像,该镜像打包了一个简单的 Node.js 应用程序,该应用程序公开了一个端点以打印 Hello World。在继续之前,请确保您已在本地计算机上安装了 Docker CE(社区版)。您可以在docs.docker.com/install/找到根据不同操作系统安装和启动 Docker 的说明。

现在创建一个名为hello-world 的目录,并在其中创建一个名为 app.js 的文件。在该文件中放置以下内容:

const http = require('http');

const name = 'node-hello-world';
const port = '8888';

const app = new http.Server();

app.on('request', (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('Hello World');
  res.end('\n');
});

app.listen(port, () => {
  console.log(`${name} is listening on port ${port}`);
});

现在在相同的目录中创建一个名为 Dockerfile 的文件,并将以下内容放入其中:

FROM node:carbon

WORKDIR /usr/src/app

COPY . ./

EXPOSE 8888

CMD [ "node", "app.js" ]

我们将构建 Docker 镜像的指令放在 Docker 文件中。您可以在docs.docker.com/engine/reference/builder/找到指令列表。

以下是之前的 Dockerfile 如何工作的:

  • 首先,您需要定义从哪个镜像构建。在这里,我们将使用 Docker Hub 提供的最新的 长期支持(LTS) 版本的 Node 的碳镜像。该镜像已经安装了Node.jsnpm

  • 接下来,我们创建一个目录来存放图像内部的应用程序代码; 这将是您应用程序的工作目录。

  • 要将您的应用程序源代码捆绑到 Docker 镜像中,我们使用COPY 指令。这里表示我们正在从当前主机操作系统的工作目录复制到 Docker 的工作目录。

  • 您的应用程序绑定到端口8888,因此您将使用 EXPOSE 指令让 Docker 守护程序进行映射。

  • 最后但同样重要的是,使用 CMD 定义运行应用程序的命令,该命令定义了您的运行时。

这是如何构建 Docker 镜像的:

  1. 使用docker build -t nodejs-hello-world . 命令构建 Docker 镜像。

  2. 要运行容器,请运行docker run -p 8090:8888 -d nodejs-hello-world 命令。

  3. -p选项将容器的端口8888绑定到主机机器的127.0.0.1的 TCP 端口8090。您也可以指定 udp 和 sctp 端口。在您的 web 浏览器中访问http://localhost:8090/,您将看到 Hello World 消息。

在 Dockerfile 中,使用ENTRYPOINT定义命令,使用CMD定义命令的参数。默认入口点是["/bin/sh", "-c'],实际运行的是sh shell。因此,在上述 Dockerfile 中,主命令是启动sh shell,并将要运行的应用程序命令作为子命令传递。-c选项接受要在sh shell 内运行的命令。

理解微服务架构

微服务架构是用于构建企业级应用程序的应用程序架构。要理解微服务架构,首先需要了解其相反的单体架构。在单体架构中,服务器端应用的不同功能组件(例如支付处理,账户管理,推送通知等组件)都融合在单个单元中。

例如,应用程序通常分为三个部分。部分是在用户计算机上运行的 HTML 页面或本地 UI,一种在服务器上运行的服务器端应用,以及在服务器上也运行的数据库。服务器端应用负责处理 HTTP 请求,在数据库中检索和存储数据,并执行算法。如果服务器端应用是一个单独的可执行文件(即,运行在单个进程中)来完成所有这些任务,那么我们说服务器端应用是单体的。这是构建服务器端应用的常见方式。几乎每个主要的 CMS、Web 服务器和服务器端框架都是使用单体架构构建的。这种架构可能看起来很成功,但当您的应用程序庞大复杂时,可能会出现问题。

在微服务架构中,服务器端应用被分为服务。一个服务(或微服务)是完整服务器端应用的特定功能的小型独立进程的组成部分。例如,您可以有一个用于支付处理的服务,另一个用于账户管理的服务,依此类推;服务需要通过网络相互通信。

服务可以通过 REST API 或消息队列相互通信,具体取决于您是否需要通信是同步还是异步的。

以下是使用微服务架构的一些好处:

  • 因为服务通过网络通信,它们可以使用不同的编程语言和不同的框架编写

  • 对服务进行更改只需要重新部署特定的服务,而不是所有服务,这是一种更快的流程。

  • 由于每个服务在不同的进程中运行,因此更容易衡量每个服务消耗了多少资源。

  • 它变得更容易测试和调试,因为可以分析每个服务。

  • 服务可以通过网络调用与其他应用程序重复使用。

  • 小团队可以并行工作,比大团队可以更快地迭代。

  • 较小的组件占用的资源较少,可以按需缩放以满足该组件的需求增加

不必在不同的虚拟机中运行每个微服务,也就是说,可以在单个虚拟机中运行多个服务。服务器与服务的比例取决于不同的因素。一个常见的因素是所需的资源和技术的数量和类型。例如,如果一个服务需要大量的 RAM 和 CPU 时间,最好是在服务器上单独运行它。如果有一些不需要太多资源的服务,可以一起在单个服务器上运行它们。

深入 K8s

一旦创建了几个 Docker 容器,您会意识到缺少了一些东西。如果要在多台机器上运行多个容器 - 如果您使用微服务,这是必须要做的 - 那么仍然有很多工作要做。

您需要在正确的时间启动正确的容器,找出它们如何相互通信,处理存储方面的考虑,并处理失败的容器或硬件。如果手动执行所有这些工作将是一场噩梦。幸运的是,这就是 K8s 发挥作用的地方。

K8s 是一个开源的容器编排平台,可以使大量的容器在一起协同工作,从而减轻运维负担。它有助于诸如:

  • 在许多不同的机器上运行容器。

  • 根据需求的变化增加或删除容器以进行扩展或缩减。

  • 保持多个应用程序实例的存储一致。

  • 在容器之间分配负载。

  • 如果有什么失败,可以在不同的机器上启动新容器,也就是自动修复。

  • 与 K8s 兼容的应用程序可以在不更改应用程序源代码的情况下轻松地由一个 IaaS 移动到另一个 IaaS。应用程序部署在 K8s 集群上,K8s 集群部署在 IaaS 上。

从开发者的角度来看,在 K8s 集群中有两种类型的机器:主节点和节点(也称为工作节点)。我们的应用程序在节点上运行,而主节点控制节点并公开 K8s API。可以在裸机上或在虚拟机上安装 K8s。还有可用的 Kubernetes 作为服务云解决方案,可以按需为您创建集群。例如:Google Cloud 的 Kubernetes Engine,Azure Kubernetes ServiceAKS)和亚马逊弹性容器服务 for KubernetesAmazon EKS)。

进入资源对象

您可以使用 K8s API 通过 K8s API 端点读取、写入和更新 K8s 资源对象。K8s 资源对象是用于表示集群状态的实体。我们需要使用清单来定义资源对象。在 API 调用中,我们传递清单文件的内容。

这是 K8s API 提供的资源的基本类别的高级概述。它们的主要功能如下:

  • 工作负载:这些资源用于在集群上管理和运行您的容器。例如:部署、Pod、作业和副本集。

  • 发现和负载平衡:这些资源用于将您的工作负载组合成一个外部可访问的、负载平衡的服务。例如:服务和入口。

  • 配置和存储:这些资源用于向您的应用程序注入初始化数据,并持久保存容器外的数据。例如:配置映射、秘密和卷。

  • 集群:这些对象定义了集群本身的配置方式;这些通常只被集群操作员使用。

  • 元数据:这些资源用于配置集群中其他资源的行为。例如:网络策略和命名空间。

Dockerfile 允许您指定关于如何运行容器的大量信息,比如要公开的端口、环境变量以及容器启动时要运行的命令。但是 K8s 建议您将这些信息移到 K8s 清单文件中,而不是 Dockerfile 中。现在,Dockerfile 只指定了如何构建和打包应用程序。此外,K8s 清单会覆盖 Dockerfile 中的指令。

部署和 Pod

K8s 鼓励您将部署视为微服务的表示。例如:如果您有五个微服务,您需要创建五个部署,而一个 Pod 是一个微服务的实例。假设您想运行三个微服务实例并在它们之间分配流量,那么在部署中您将定义您需要三个副本,这将创建三个 Pod。一个 Pod 运行一个或多个代表微服务的容器。

在创建部署时,您可以指定微服务需要的计算资源量,比如内存和 CPU,而不是让它消耗所有可用资源。您还可以指定一个节点名称来运行 Pod,而不是由 K8s 决定。

在创建部署时,您可以指定要公开的 Docker 容器的哪些端口、环境变量和其他各种在 Dockerfile 中也指定的内容。

服务

默认情况下,部署之间无法相互通信。服务被创建用于启用微服务之间的通信,并可选地允许从集群外部访问微服务。我们需要为每个部署创建一个服务。服务具有内置的负载均衡功能:如果一个微服务有三个 pod,那么 K8s 服务会自动在它们之间分配流量。以下是各种类型的服务:

  • ClusterIP:这是默认的服务类型。在集群中的内部 IP 上暴露服务。此类型使得服务仅可从集群内部访问。

  • NodePort:使服务可以从集群外访问。它是ClusterIP的超集。当我们创建一个类型为NodePort的服务时,K8s 会在30000-32767范围内打开一个或多个端口(取决于 Docker 容器暴露的端口数量),并将它们映射到所有工作节点的容器端口。因此,如果一个微服务的实例没有在运行,比如说在第三台机器上,仍然可以在第三台机器上暴露端口。K8s 处理内部路由。因此,您可以使用任何工作节点的公共 IP 与分配的端口来访问微服务。如果您不希望 K8s 在外部暴露时在30000-32767之间选择随机端口,则可以指定同一范围内的一个端口。

  • LoadBalancer:也用于在集群外部暴露服务。它将在服务前面启动一个负载均衡器。这仅在支持的云平台上有效,例如 AWS、GCP 和 Azure。

Ingress 控制器和资源

Ingress 是一个用于在集群外部负载均衡和暴露微服务的 K8s 功能。与 NodePort 和 LoadBalances 相比,它是功能丰富且推荐的负载均衡和暴露微服务的方式。Ingress 为您提供了一种根据请求主机或路径路由请求到服务的方式,从而将许多服务集中到单个入口点中,这样更容易管理大型应用程序。Ingress 还支持 SSL 卸载、URL 重写和许多其他功能,因此您不必在创建每个微服务时集成所有这些功能。

Ingress 分为两个主要部分:Ingress 控制器和资源。Ingress 控制器是暴露在集群外部的实际反向代理,Ingress 资源是控制器的配置。Ingress 控制器本身是一个微服务,也就是说,它是一个部署,并为其创建了一个类型为NodePortLoadBalancer的服务。Ingress 控制器具有读取 Ingress 资源并重新配置自身的能力。

有各种不同的 Ingress 控制器实现可用,你应该选择最适合你目的的那个。它们根据特性和使用的负载均衡器和反向代理软件而变化。K8s 官方开发了 NGINX Ingress 控制器,它是 K8s 最常见的 Ingress 控制器。该 Ingress 控制器实现使用了 NGINX 反向代理和负载均衡器。

在部署 Ingress 控制器时,你可以有一个以上的副本以获得高可用性和 Ingress 的负载均衡。你也可以部署多个 Ingress,它们使用类别进行区分。

配置映射和密码

几乎每个应用程序在运行之前都需要传递某种类型的配置。例如,当启动一个 Node.js 应用程序时,你可能需要传递 MongoDB 的 URL,因为你不能硬编码它,因为它在开发和生产环境中不同。这些配置通常作为环境变量或配置文件提供。

K8s 允许你在部署清单中指定环境变量。但是,如果你想要更改它们,你必须修改部署。更糟糕的是,如果你想要在多个部署中使用变量,你必须复制数据。K8s 提供了配置映射(用于非机密数据)和密码(用于机密数据)来解决这个问题。

密码和配置映射的主要区别在于密码使用 Base64 编码进行混淆。现在,你可以将配置映射和密码作为部署清单的环境变量传递。当配置映射或密码发生更改时,环境变量也会相应更改,无需任何重启或手动操作。

如果你的应用程序使用配置文件而不是环境变量,它们也可以使用配置映射和密码进行传递。

绑定挂载和卷

在 K8s 和 Docker 中,绑定挂载是将主机上的文件或目录挂载到容器中的过程。文件或目录通过主机上的完整或相对路径引用。

在计算机数据存储中,卷是具有单个文件系统的持久存储区域,通常(尽管不一定)驻留在硬盘的单个分区上。IaaS 提供商允许我们创建卷并附加到 VM。K8s 提供了名为持久卷和持久卷声明的功能,可以自动创建特定云提供商的卷并附加到 pod。当你的应用程序需要保存(持久化)数据时,卷是必需的。这些卷通过绑定挂载在 Docker 容器内部访问。

在 K8s 中,有一个名为StatefulSets的资源对象,它类似于部署。如果你的部署需要持久性存储,并且你有多个副本,那么你必须创建 StatefulSets 而不是部署,因为部署不能为每个 pod 分配单独的持久性卷。

标签和选择器

标签是附加到资源对象(例如 pod、service 和 deployment)的键/值对。标签用于指定对象的识别属性,这些属性对用户来说是有意义且相关的。标签可用于组织和选择对象的子集。在创建时间或随后的任何时间,可以向对象添加和修改标签。每个对象可以定义一组键/值标签。例如,在创建服务时,我们使用标签和选择器指定应该暴露的 pod 列表。

开始使用 minikube

当您构建真实的应用程序时,正确使用 K8s 的方式是在本地或云端创建一个开发集群,具体取决于您是将应用程序托管在本地还是云上。但是,为了对 K8s 进行实验和玩耍,您可以使用 minikube。

Minikube 是一个工具,可以方便地在本地运行 K8s。Minikube 在您的笔记本电脑上的虚拟机内运行单个工作节点 K8s 集群,供用户尝试 K8s 或进行日常开发使用。在撰写本书时,minikube 的最新版本是 0.26.1。Minikube 可以安装在 Windows、macOS 和 Ubuntu 上。

在 macOS 上安装 minikube

首先,安装 minikube 支持的虚拟机监视程序。在 macOS 上,建议使用 hyperkit。使用以下命令安装 hyperkit 驱动程序:

curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
&& chmod +x docker-machine-driver-hyperkit \
&& sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \
&& sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \
&& sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit

然后安装 kubectlkubectl 是一个命令行工具,用于部署和管理 K8s 上的应用程序。以下是安装它的命令:

brew install kubectl

现在,使用以下命令安装 minikube:

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.26.1/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

在 Ubuntu 上安装 minikube

在 Ubuntu 上,建议使用 hyperkit。使用以下命令安装 hyperkit:

curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
&& chmod +x docker-machine-driver-hyperkit \
&& sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \
&& sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \
&& sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit

然后安装 kubectl。以下是安装它的命令:

sudo snap install kubectl --classic

现在,使用以下命令安装 minikube

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.26.1/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

在 Windows 上安装 minikube

在 Windows 上,建议使用 VirtualBox 虚拟机监视程序。从 www.virtualbox.org/wiki/Downloads 下载并安装 VirtualBox。

然后从 storage.googleapis.com/kubernetes-release/release/v1.10.0/bin/windows/amd64/kubectl.exe 下载 kubectl 命令。

最后,通过下载并运行 minikube 安装程序来安装 minikube,网址为 github.com/kubernetes/minikube/releases/download/v0.26.1/minikube-installer.exe

启动 minikube

在 Linux 和 macOS 上,使用以下命令启动 minikube:

minikube start --vm-driver=hyperkit

在 Windows 上,使用以下命令启动 minikube:

minikube start --vm-driver=virtualbox

如果您使用不同的虚拟机监视程序,请更改 --vm-driver 选项的值。启动 minikube 需要几分钟。

停止和删除 minikube

如果您想要随时停止 minikube 集群,可以使用以下命令:

minikube stop

你可以使用上述 minikube start 命令重新启动相同的集群。如果想删除整个集群,则可以使用以下命令:

minukube delete

Minikube 状态

要检查 minikube 的状态,即集群是否正在运行,可以使用以下命令:

minikube status

如果成功运行,你将看到类似于这样的响应:

minikube: Running
cluster: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.64.7

注意这里你会看到一个不同的 IP 地址。这是 minikube 虚拟机的 IP 地址;也就是说,主节点和工作节点在这个虚拟机内运行。你将从这个 IP 访问你的应用。

访问 K8s 仪表板

K8s 仪表板是一个通用的、基于 Web 的 K8s 集群 UI。它允许用户管理运行在集群中的应用程序,并对其进行故障排除,还有集群本身。要访问仪表板,请运行此命令:

minikube dashboard

它将在新的浏览器窗口中打开仪表板。K8s 仪表板将类似于以下内容:

在 k8s 上部署 Hello World 应用

让我们将之前构建的 Hello World Docker 镜像部署到我们刚创建的 K8s 集群上。要创建一个部署和服务,你需要创建一个包含有关部署和服务的所有详细信息的清单文件,然后使用 kubctl 命令将其传递给 K8s。在清单文件中,你需要提供 Docker 镜像的远程 URL,以便 K8s 拉取并运行这些镜像。K8s 可以从公共 Docker 注册表(即 Docker Hub)或私有 Docker 注册表中拉取镜像。

将镜像推送到 Docker Hub

在我们推送镜像之前,让我们了解一些与 Docker 相关的基本术语:

  • 注册表:存储你的 Docker 镜像的服务。

  • 仓库:不同 Docker 镜像的集合,它们具有相同的名称但具有不同的标签(版本)。

  • 标签:你可以使用它来区分 Docker 镜像的不同版本,以便保留旧副本。当我们之前创建 Docker 镜像时,我们没有提供标签,因此默认标签是 latest。可以使用 docker tag [:HOST|:USERID]IMAGE_NAME[:TAG_NAME] [:HOST|:USERID]IMAGE_NAME[:TAG_NAME] 命令从另一个镜像创建一个带标签的新镜像。主机前缀是可选的,用于指示 Docker 注册表的主机名,如果镜像属于私有 Docker 注册表。如果镜像用于 Docker Hub,则提及你的 Docker Hub 帐户的用户名。

要将镜像推送到 Docker Hub,你首先需要创建一个 Docker Hub 帐户。访问 hub.docker.com 并创建一个帐户。登录后,你将看到类似以下的屏幕:

现在点击“创建仓库”并填写以下表格:

可见性指示存储库是私有还是公共的。私有存储库不对所有人可见。如果您有权限访问它,则需要登录到 Docker Hub 才能拉取它。您只能在 Docker Hub 上创建一个免费私有存储库。创建存储库后,您将看到一个类似于此的屏幕:

要推送您在本地计算机上的映像,您需要首先从命令行登录到 Docker Hub。要执行此操作,请运行以下命令:

docker login

然后在提示时键入您的 Docker Hub 帐户的用户名和密码。您应该看到登录成功的消息。现在使用以下命令为您的映像打标签:

docker tag nodejs-hello-world:latest narayanprusty/nodejs-hello-world

现在运行以下命令来推送映像:

docker push narayanprusty/nodejs-hello-world

根据您的互联网带宽不同,推送可能需要几分钟时间。推送完成后,点击存储库上的 Tags 选项卡,您将看到一个类似于此的屏幕:

创建部署和服务

现在,让我们创建包含有关部署和服务信息的主要清单文件。我们可以为我们的部署和服务创建两个不同的或单个部署文件。主要清单文件可以用 YAML 或 JSON 格式编写。首选 YAML,因此我们也将以 YAML 格式编写。

创建一个名为 helloWorld.yaml 的文件,并将以下内容放入其中:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: hello-world
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
      - name: nodejs-hello-world
        image: narayanprusty/nodejs-hello-world
        command: [ 'node', 'app.js']
        workingDir: /usr/src/app
        imagePullPolicy: Always
        ports:
        - containerPort: 8888
---
kind: Service
apiVersion: v1
metadata:
  name: hello-world
spec:
  ports:
    - name: api
      port: 8888
  selector:
      app: hello-world
  type: NodePort

在前面的主要清单文件中,大多数内容都是不言自明的。在这里,您会注意到我们有一个名为 imagePullPolicy 的字段。默认的映像拉取策略是 IfNotPresent,这会导致 K8s 如果映像已存在则跳过拉取。如果您想始终强制拉取,可以使用 Always 策略、:latest 标签或不带标签。

command 在 K8s 中与 Dockerfile 的 ENTRYPOINT 相同。K8s 中的 arguments 与 Dockerfile 中的 CMD 相同。如果未为容器提供命令或 args,则使用 Docker 映像中定义的默认值。如果为 Container 提供了命令但没有 args,则仅使用提供的命令。忽略 Docker 映像中定义的默认 ENTRYPOINT 和默认 CMD。如果仅为 Container 提供了 args,则使用 Docker 映像中定义的默认 ENTRYPOINT 运行您提供的 args。如果提供了命令和 args,则忽略 Docker 映像中定义的默认 ENTRYPOINT 和默认 CMD。将使用您的 args 运行您的命令。

现在使用以下命令将主要清单提供给 K8s:

kubectl apply -f helloWorld.yaml

apply 子命令用于将主要清单文件提供给 K8s。如果要更新部署或服务配置,请更改文件并重新运行命令。在成功执行上述命令后,打开 K8s 仪表板,您将看到部署和服务已成功创建。

现在,要向容器发出 HTTP 请求,我们需要服务提供的 worker 节点 IP 和端口号。使用 minikube ip 命令查找 IP,并在 K8s 仪表板中打开服务以查找暴露的端口号,如下面的屏幕截图所示:

在我的情况下,端口号是 31474。您将看到不同的端口号。使用端口号和 IP 在浏览器中发出请求,您将看到Hello World消息。

要删除部署,使用 kubectl delete deployment deployment_name 命令,并且要删除服务,请使用 kubectl delete svc service_name 命令。

构建 QaaS

现在让我们开始构建一个 QaaS 平台,这样我们就可以通过点击按钮来部署、创建和加入网络。正如您所知,启动 Quorum 节点需要许多手动步骤,如创建 genesis.json 文件、 static-nodes.json 文件和 enode。由于我们的目标是自动执行所有这些步骤,因此我们需要编写自动化脚本来执行这些步骤。因此,我们将使用Quorum Network ManagerQNM),它允许用户轻松创建和管理 Quorum 网络,无需任何手动步骤。

QNM 是 Quorum 的开源封装,旨在简化 Quorum 网络的设置。当您使用 QNM 时,您不再需要担心 enode、钱包、创世文件、static-nodes.json 文件等。您可以在官方 QNM 仓库找到。目前,QNM 的最新版本是 v0.7.5-beta

请注意,QNM 目前仅与 Ubuntu 16.04 兼容。

在我们的 QaaS 中,我们将 Quorum 节点部署为 K8s 中的部署。每当您想要启动网络或加入现有网络时,都会创建一个新的部署。QNM 未经容器化,因此我们构建 QaaS 的第一步是对其进行容器化。

QNM 如何工作?

在将 QNM 容器化之前,让我们了解一下它的工作原理。第一步是安装 QNM。有两种安装 QNM 的方式:通过运行提供的安装脚本(setup.sh 文件)或手动安装。我们将通过运行脚本来安装它。该脚本会负责安装使用 QNM 所需的所有内容。

您可以使用 node setupFromConfig.js 命令使用 QNM 启动 Quorum 节点。运行 QNM 节点时提供配置的两种方式:使用 config.js 文件或使用环境变量。您还可以使用 node index.js 命令启动节点,这将提供一个交互式方式来配置节点。

在 QNM 中,要创建一个网络,您必须执行以下步骤:

  1. 创建一个协调节点

  2. 动态地向网络添加节点

网络的第一个节点应该是协调节点;其他动态添加的节点是非协调节点。其他动态添加的节点连接到协调节点以获取与网络相关的信息和配置。

唯一需要注意的是,在启动第一个节点时,确保它是一个协调节点。在启动其他动态节点时,请确保提供协调节点的 IP 地址。

剩下的流程由 QNM 自动处理。

将 QNM 容器化

Dockerfile 用于将 QNM Docker 化,将涉及安装 QNM。以下是 Dockerfile 的内容:

FROM ubuntu:16.04

#Install Utilities
RUN apt-get update
RUN apt-get install -y --no-install-recommends vim less net-tools inetutils-ping wget curl git telnet nmap socat dnsutils netcat tree htop unzip sudo software-properties-common jq psmisc iproute python ssh rsync gettext-base

# Install QNM
RUN mkdir -p workspace && cd workspace && wget https://raw.githubusercontent.com/ConsenSys/QuorumNetworkManager/v0.7.5-beta/setup.sh && chmod +x setup.sh && ./setup.sh
ENV LANGUAGE=en_US.UTF-8 LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
RUN apt-get install -y locales && locale-gen en_US.UTF-8

WORKDIR /workspace/QuorumNetworkManager
ENTRYPOINT ["/bin/bash", "-i", "-c"]

以下是前述 Dockerfile 的工作方式:

  • 我们使用了 Ubuntu 16.04 基础镜像。

  • 我们安装了几个基本工具。

  • 我们使用给定的命令安装了 QNM,命令位于 github.com/ConsenSys/QuorumNetworkManager/releases/tag/v0.7.5-beta

  • 我们将工作目录设置为 workspace/QuorumNetworkManager,在其中有启动节点的 QNM 文件。

  • 我们更改了入口点以使用 bash shell 而不是 sh shell,因为 QNM 在 sh shell 上不起作用。QNM 在交互模式下执行时将路径设置为 ~/.bashrc 文件中的各种二进制文件,该文件由 bash shell 加载。

继续将镜像推送到 Docker Hub。我已经将镜像推送到 narayanprusty/qnm

创建 QNM 部署和服务主清单文件

让我们编写为创建 QNM 的部署和服务的主清单文件。我们将仅创建用于创建 Raft 网络的部署,但是你可以轻松扩展以支持 IBFT。

下面是为基于 Raft 的协调节点创建部署和服务的主清单文件:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: coordinator
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: coordinator
    spec:
      containers:
      - name: qnm
        image: narayanprusty/qnm
        args: ['node setupFromConfig.js']
        workingDir: /workspace/QuorumNetworkManager
        imagePullPolicy: Always
        env: 
        - name: IP
          value: 0.0.0.0
        ports:
        - containerPort: 50000
        - containerPort: 50010
        - containerPort: 50020
        - containerPort: 20000
        - containerPort: 20010
        - containerPort: 20020
        - containerPort: 40000
        - containerPort: 30303
        - containerPort: 9000
---
kind: Service
apiVersion: v1
metadata:
  name: coordinator
spec:
  ports:
    - name: remote-communication-node
      port: 50000
    - name: communication-node-rpc
      port: 50010
    - name: communication-node-ws-rpc
      port: 50020
    - name: geth-node
      port: 20000
    - name: geth-node-rpc
      port: 20010
    - name: geth-node-ws-rpc
      port: 20020
    - name: raft-http
      port: 40000
    - name: devp2p
      port: 30303
    - name: constellation
      port: 9000
  selector:
      app: coordinator
  type: NodePort

在这里,环境变量 IP 用于指示节点应该侦听的 IP。0.0.0.0 表示任何 IP。然后,我们暴露了由 QNM 打开的端口。前面的主清单文件中的所有内容都是不言自明的。

现在让我们为一个动态节点创建主清单文件:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: non-coordinator
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: non-coordinator
    spec:
      containers:
      - name: qnm
        image: narayanprusty/qnm
        args: ['node setupFromConfig.js']
        workingDir: /workspace/QuorumNetworkManager
        imagePullPolicy: Always
        env:
        - name: COORDINATING_IP
          value: 10.97.145.237
        - name: ROLE
          value: dynamicPeer 
        - name: IP
          value: 0.0.0.0
        ports:
        - containerPort: 50000
        - containerPort: 50010
        - containerPort: 50020
        - containerPort: 20000
        - containerPort: 20010
        - containerPort: 20020
        - containerPort: 40000
        - containerPort: 30303
        - containerPort: 9000
---
kind: Service
apiVersion: v1
metadata:
  name: non-coordinator
spec:
  ports:
    - name: remote-communication-node
      port: 50000
    - name: communication-node-rpc
      port: 50010
    - name: communication-node-ws-rpc
      port: 50020
    - name: geth-node
      port: 20000
    - name: geth-node-rpc
      port: 20010
    - name: geth-node-ws-rpc
      port: 20020
    - name: raft-http
      port: 40000
    - name: devp2p
      port: 30303
    - name: constellation
      port: 9000
  selector:
      app: non-coordinator
  type: NodePort

这个主清单文件看起来与之前的主清单文件非常相似,只是环境变量不同。在这里,我们提供了协调节点的 IP 地址。IP 地址是由协调节点服务暴露的集群 IP。对于你来说应该是不同的。然后,我们有 ROLE 环境变量,以指示 QNM 是动态节点而不是协调节点。

使用 K8s API 创建节点

K8s 主服务器公开了可用于读取和写入 K8s 资源对象的 API。你可以在 kubernetes.io/docs/reference/ 找到 API 参考文档。对于 QaaS,你需要创建一个前端,内部调用这些 API 来创建部署和服务。

访问 K8s APIs 最简单的方式是通过 HTTP 代理。Kubectl 允许你在本地主机和 K8s API 服务器之间创建代理服务器。所有进入的数据都通过一个端口进入,并转发到远程 K8s API 服务器端口,除了与静态内容路径匹配的路径。要创建代理服务器,请使用以下命令:

kubectl proxy --address="0.0.0.0" -p 8000

让我们看一个使用Node.js为协调节点创建部署的示例:

var request = require("request");

var options = {
    method: 'POST',
    url: 'http://127.0.0.1:8000/apis/apps/v1beta1/namespaces/
      default/deployments',
    headers: {
        'Content-Type': 'application/json'
    },
    body: {
        apiVersion: 'apps/v1beta1',
        kind: 'Deployment',
        metadata: {
            name: 'coordinator'
        },
        spec: {
            replicas: 1,
            template: {
                metadata: {
                    labels: {
                        app: 'coordinator'
                    }
                },
                spec: {
                    containers: [{
                        name: 'qnm',
                        image: 'narayanprusty/qnm',
                        args: ['node setupFromConfig.js'],
                        workingDir: '/workspace/QuorumNetworkManager',
                        imagePullPolicy: 'Always',
                        env: [{
                            name: 'IP',
                            value: '0.0.0.0'
                        }],
                        ports: [{
                                containerPort: 50000
                            },
                            {
                                containerPort: 50010
                            },
                            {
                                containerPort: 50020
                            },
                            {
                                containerPort: 20000
                            },
                            {
                                containerPort: 20010
                            },
                            {
                                containerPort: 20020
                            },
                            {
                                containerPort: 40000
                            },
                            {
                                containerPort: 30303
                            },
                            {
                                containerPort: 9000
                            }
                        ]
                    }]
                }
            }
        }
    },
    json: true
};

request(options, function(error, response, body) {
    if (error) throw new Error(error);

    console.log(body);
});

类似地,让我们看一个使用Node.js为协调节点创建服务的示例:

var request = require("request");

var options = {
    method: 'POST',
    url: 'http://127.0.0.1:8000/api/v1/namespaces/default/services',
    headers: {
        'Content-Type': 'application/json'
    },
    body: {
        kind: 'Service',
        apiVersion: 'v1',
        metadata: {
            name: 'coordinator'
        },
        spec: {
            ports: [{
                    name: 'remote-communication-node',
                    port: 50000
                },
                {
                    name: 'communication-node-rpc',
                    port: 50010
                },
                {
                    name: 'communication-node-ws-rpc',
                    port: 50020
                },
                {
                    name: 'geth-node',
                    port: 20000
                },
                {
                    name: 'geth-node-rpc',
                    port: 20010
                },
                {
                    name: 'geth-node-ws-rpc',
                    port: 20020
                },
                {
                    name: 'raft-http',
                    port: 40000
                },
                {
                    name: 'devp2p',
                    port: 30303
                },
                {
                    name: 'constellation',
                    port: 9000
                }
            ],
            selector: {
                app: 'coordinator'
            },
            type: 'NodePort'
        }
    },
    json: true
};

request(options, function(error, response, body) {
    if (error) throw new Error(error);

    console.log(body);
});

摘要

在本章中,我们通过示例学习了云计算和容器化的基础知识。我们看到了容器化的重要性以及如何使用 Docker 对应用程序进行容器化。然后,我们看到了 K8s 的重要性,以及它如何使基于微服务架构的应用程序开发变得容易。之后,我们学习了如何安装 minikube 并在 K8s 上部署容器。

最后,我们利用学到的所有技能来开发一个基于 QNM 的 QaaS 服务。在下一章中,我们将创建一个调用 K8s API 创建和加入网络的 QaaS 的基本 UI。

第七章:为数字化医疗记录构建 DApp

整个医疗行业都充斥着大量的纸质医疗记录,这导致了巨大的经济、时间和生命损失。电子医疗记录(EMRs)是纸质记录引起的许多问题的解决方案。有许多公司和研究人员正在利用区块链技术构建 EMR 数据管理和共享系统。我们将设计一种与互联网上的解决方案非常不同的解决方案,因为那些解决方案仅专注于匿名性、访问控制、安全性和隐私,而我们的解决方案还将通过实现跨应用程序通信来提供用户体验和大规模采用。在构建系统时,我们将学习如何使用 Proxy Re-Encryption(PRE)实现隐私。

在本章中,我们将学习以下主题:

  • 什么是医疗数据管理系统,以及它的功能是什么?

  • 纸质医疗记录引起的问题以及数字化医疗记录的好处

  • 医疗数据管理系统的限制?

  • 构建集中式医疗数据管理应用程序的问题

  • 什么是 PRE 以及它如何帮助实现区块链中的数据隐私

  • 如何设计一个去中心化的医疗数据管理系统,实现跨应用程序的数据共享

  • 如何使用 Python 和 JavaScript 为医疗数据管理的 DApp 构建智能合约和测试

介绍 EMRs 数据管理和共享系统

电子医疗记录(EMRs)包含了医疗保健领域中的关键、高度敏感的私人信息,并且需要在同行之间频繁共享。EMR 数据管理和共享系统促进了不同参与者以安全可信的方式读取和写入 EMR 到系统中。这些系统应确保 EMR 数据的隐私、安全、可用性和细粒度的访问控制。EMRs 包括处方、实验室报告、账单以及在医院中找到的任何其他纸质记录。

一般来说,EMR 数据管理和共享系统允许医生开具数字处方,药店根据患者身份提取处方,实验室发出数字报告,患者查看所有记录并与他人共享等。

纸质医疗记录存在的问题

医疗记录需要在医疗过程中分发和共享给同行,如医疗服务提供者、保险公司、药店、研究人员和患者家属,这本身就是一个挑战。即使在共享后,这些记录在治疗过程中也需要不断更新。纸质记录也更容易丢失或放错位置。当有人患有严重的医疗状况,如癌症或艾滋病时,他们必须维护长时间的记录,因为这对治疗至关重要。使用纸质记录时,维护长时间的记录很麻烦。

此外,伪造的健康记录被提交给保险公司,导致保险公司巨大的财务损失。很多时候,医生和实验室也会在患者的同意下发布虚假处方和记录。例如,大学要求学生通过几项测试才能被录取,有时学生会试图获取假报告而不参加测试。

许多患者不购买自己的药物,也没有办法追踪患者是否服用了必要的药物。这导致患者生活质量受损,医疗系统成本增加。如果患者去不同的医生那里就诊,那么由于不同医生推荐不同类型的药物,患者出现有害副作用的可能性很大。如果一个人有多个医生为其治疗,那么这些医生就无法共同制定患者的药物管理计划,因此无法为所有相关方简化整个流程。由于患者未能提供他们的过去记录,当他们在不同的诊所就诊时,需要再次进行与特定化学品或物质相关的过敏测试,如果患者的医疗史得以保持,则无需进行此类测试。

处方中的潦草字迹也可能导致用药错误的风险。此外,由于远程通信时医生之间的口头交流,用药错误的可能性很大。此外,由于开具处方者对药物的期望剂量或多种药物之间的不良相互作用了解不足,也没有办法在纸质处方中实施警告和警报系统。

对于研究公司来说,收集和整理医疗记录用于研究目的是困难的。用药结束后无法续开处方,因此患者需要再次看医生,这是一个繁琐的过程。纸质处方无法实现在线购买药物,但数字化处方可以为在线药物配送打开大门,因为处方可以在线验证。

电子医疗记录(EMR)数据管理和共享系统应旨在解决之前的一些或所有问题。例如,在处方续开的情况下,第一步是数字化处方。然后,在患者的请求下,药房工作人员可以生成一个续开请求,该请求将被送达给开具处方的人员。开具处方的人员随后可以审查请求,并根据情况批准或拒绝请求。通过有限的资源利用和从开具处方者那里点击几下,他们就可以完成药物续开任务,同时增强持续的患者文档。

前述问题只是纸质记录带来的一些问题之一。但整个医疗保健行业充斥着大量由纸质记录引起的问题。解决方案应设计成能够解决这些问题,并且可以定期增强以解决额外的问题。

电子病历数据管理和共享系统的局限性

尽管电子病历数据管理和共享系统解决了很多问题,但它也有一些限制,影响了它的适应性和人们对其的信任。以下是一些限制:

  • 财务成本和投资回报:购买、实施、支持和维护此类系统的成本是无法承受的,特别是对于小型医院和诊所来说。即使他们免费获得系统,也会有其他与界面管理、灵活性定制、培训、维护和升级相关的财务成本。

  • 升级员工队伍:目前,员工沉迷于纸质记录。培训患者、医生、药店、医院等接受解决方案是一项困难且耗时的任务。有时可能需要改变员工队伍。例如,当银行开始实施计算机,从记账簿转向数字记录时,许多人无法理解和接受,因此失去了工作。

  • 数据输入的完整性:意外的数据输入错误,比如选择了错误的患者或在剂量菜单中点击了错误的选择,可能会发生。

  • 安全与隐私:这是最重要的关注之一。健康记录需要安全存储,因为电子健康数据库始终是黑客攻击的目标。健康记录包含非常敏感的信息,泄露可能会导致灾难。应实施严格的访问控制,并定期征求反馈意见。在没有患者同意的情况下,他们的记录不应与任何人共享。

  • 系统停机时间:由于网络或硬件相关问题,定期发生系统停机的可能性很高。无法使用系统是一个大问题。

  • 患者丢失访问权:在患者无法控制的情况下,比如医疗提供者办公室的软件故障,患者无法再向医疗提供者要求纸质处方以去药房购买所需的药物。这使患者完全依赖技术人员或其他不可发现的工作者。

集中化与去中心化的电子病历管理系统

无论 EMR 管理系统是集中式还是分散式,它都必须符合卫生当局的法律法规。由于这是敏感的公共数据,应该有一个监管机构来定义标准并规定数据的分享和存储规则。例如,1996 年的《健康保险移植和责任法案》(HIPAA)是美国的一项立法,为保障医疗信息的数据隐私和安全提供了规定。同样,不同国家有不同的立法。

要理解集中式 EMR 数据管理应用程序的问题,让我们以谷歌健康集中服务为例。谷歌健康服务允许用户手动添加他们的健康记录到应用中,或者通过登录到谷歌合作的健康服务提供者来添加。应用中存储的记录可能包括健康状况、药物、过敏以及实验室结果。谷歌健康利用这些信息为用户提供关于医疗状况、药物、状况和过敏之间可能的互动的信息。

2011 年,谷歌宣布将于 2012 年 1 月 1 日停止谷歌健康服务。数据可通过下载方式获取直至 2013 年 1 月 1 日。谷歌放弃该项目的原因是缺乏普遍的接受程度。

这表明我们很难相信集中式应用程序,因为它们可以随时终止服务。作为患者,这是一个大问题,因为突然间你不得不寻找其他选项来管理你的记录。即使是医院和其他卫生服务提供者也将不得不改变他们的系统。即使你想转换到另一个应用,迁移数据也不容易。许多谷歌健康(Google Health)的客户转向使用微软健康宝库(Microsoft HealthVault),这是一个竞争性的服务。微软发布了一个工具,让谷歌健康的客户将他们的个人健康信息转移到微软健康宝库账户。但是如果微软也停止他们的服务呢?因此,私人公司开发的集中式卫生应用程序是不值得信赖和采用的。

因此,许多政府都推出了他们自己的集中服务。爱沙尼亚的电子处方服务就是一个例子。政府服务是可以信赖的,采用并不是问题,因为政府可以强制卫生服务提供者使用该服务。这是他们强制的权威。但问题是一个应用无法解决所有问题,提供最佳功能集,拥有最佳用户体验等等。

政府和私人的集中式应用都存在问题,例如,在出现安全漏洞时,黑客可能危及集中服务中的所有公共记录。此外,有什么保证健康记录不会被修改,或者中央服务器不会删除某些记录呢?

前述问题意味着我们需要设计一个使用区块链的分散系统,其中区块链仅用于 EMR 访问控制和身份管理,而 EMR 位于中央化和分布式存储中。连接到该网络的所有应用程序都可以相互通信并共享数据。用户可以轻松切换应用程序,卫生管理机构将能够监管和监控网络。例如,两个不同的服务提供商可以构建具有不同功能和用户体验集合的不同应用程序,但是不同应用程序的用户可以读取/写入每个应用程序的 EMR。

卫生管理机构决定谁可以加入网络并提供医疗应用程序。为了加入网络,卫生管理机构可以设定一个预先检查的标准和措施清单,应用程序必须符合这些标准和措施才能加入网络。

在区块链中使用 PRE 确保数据隐私

在进一步进行并构建我们的分布式 EMR 数据管理和共享系统之前,让我们先了解一下 PRE 是什么。在我们的解决方案中,我们将使用 PRE 来确保安全和隐私。

PRE 是一组算法,允许您使用您的密钥对一些文本进行加密,然后更改密文,以便另一方可以解密,而不会透露您的密钥。要更改密文,您需要另一方的私钥或公钥,具体取决于您是使用交互式还是非交互式 PRE 算法。无论使用哪种算法,PRE 都涉及生成重新加密密钥,该密钥用于重新加密数据。重新加密密钥是基于所有者的私钥和接收者的私钥或公钥生成的,具体取决于算法类型。

在实践中,PRE 用于在第三方服务器上存储敏感数据,并允许您决定谁可以访问数据,而不会向第三方服务器透露实际数据。PRE 允许第三方(代理)更改已为一方加密的密文,以便另一方可以解密。

与其简单地与接收方共享您的私钥(不安全)或者为每个接收方分别加密整个消息n次,PRE 允许您仅加密数据一次,然后基于接收方的公钥委派对其进行访问。这消除了数据所有者必须在线的要求(数据可以存储在您不必管理的不同服务器上),并且还便于撤销访问权限(要阻止访问,您可以再次运行 PRE 来更改您的密钥,然后删除旧的密文)。

NuCypher PRE 当前支持的 PRE 算法是 BBS98。BBS98 基于椭圆曲线密码学。该库默认使用 secp256k1 曲线。请注意,以太坊帐户也使用相同的曲线(secp256k1),因此我们可以使用以太坊帐户密钥与 NuCypher。

目前,PRE 领域仍在积极研究和开发中。目前还没有多少用于 PRE 的库可用。你会发现基于 Java 或 Python 的交互式 PRE 库,但对于非交互式或基于对称密钥的,你找不到任何库。由于这个限制,我们将坚持使用微服务架构,并将所有代理重加密代码移至基于 Python 的微服务。

NuCypher PRE 库

NuCypher 是一家构建去中心化的 PRE 服务产品的公司,称为 NuCypher 密钥管理服务KMS)。NuCypher KMS 是一种去中心化的 KMS、加密和访问控制服务。它使得在公共网络中任意数量的参与者之间进行私密数据共享成为可能,使用 PRE 来委托解密权限,这是传统的对称或公钥加密方案无法实现的。原生代币用于激励网络参与者执行密钥管理和访问委托/撤销操作。

我们不会深入研究 NuCypher KMS,也不会在本书中使用它。相反,我们将探索如何使用 NuCypher 构建的 PRE 库。NuCypher 提供了 Python 和 Java 的 PRE 库,但我们只会学习如何使用 Python PRE 库。

NuCypher 不是 PRE 的唯一 Python 库。还有其他一些库可用。例如,ZeroDB 也提供了一个支持 AFGH 算法的 PRE 库,这是一个非交互式 PRE 算法。你可以在这里了解更多信息。

安装库

此库需要 python3libssl-devlibgmp-dev 作为先决条件。要在 Ubuntu 上安装这些,请运行以下命令:

sudo apt-get install build-essential
sudo apt-get install python3
sudo apt-get install python3-dev libssl-dev libgmp-dev

在 macOS 上使用以下命令:

brew install python3
brew install gmp

现在让我们安装 PRE 库。要安装它,请运行以下命令:

git clone https://github.com/nucypher/nucypher-pre-python.git
cd nucypher-pre-python
pip3 install -e .

使用库

让我们看一个如何使用这个库的例子。该库仅支持交互式算法;它需要发送方了解接收方的私钥。

我们将创建一个示例 Python 脚本,其中 Alice 将加密一些文本,Bob 将与 Alice 分享他的私钥,Alice 将使用 Bob 的私钥创建一个派生密钥,然后 代理 将使用派生密钥进行再加密,最后 Bob 将使用他的私钥解密再加密的数据:

以下是这些交互的代码:

# Import bbs98 from NuCypher PRE
from npre import bbs98
# Initialize the re-encryption object
pre = bbs98.PRE()

# 'sk' means "secret key", and 'pk' means "public key"

# Alice's Private key
sk_a = pre.gen_priv(dtype=bytes)
# Alice's Public Key
pk_a = pre.priv2pub(sk_a)

# Bob's Private Key
sk_b = pre.gen_priv(dtype=bytes)
# Bob's Public Key
pk_b = pre.priv2pub(sk_b)

# Print Alice's Private Key as Hex String
print(sk_a.hex()[2:])
# Print Bob's Private Key as Hex String
print(sk_b.hex()[2:])

# Encrypt Message using Alice's Public Key
emsg = pre.encrypt(pk_a, "Hello World")

# Generate Re-Encrypt Key using Private key of sender and receiver
re_ab = pre.rekey(sk_a, sk_b)
# Re-Encrypt Message using Re-Encrypt key
emsg_b = pre.reencrypt(re_ab, emsg)

# Decrypt the message using Bob's Private Key
dmsg = pre.decrypt(sk_b, emsg_b)
# Print Decrypted Message
print(dmsg.decode("utf-8"))

上述代码不言自明。但上述场景中的问题是,为了让 Alice 将数据访问权限授予 Bob,Alice 需要知道 Bob 的私钥。这不是理想的情况,Bob 可能不愿意分享他的密钥。例如,如果 Bob 在使用相同的密钥进行区块链交易,那么他肯定不希望与 Alice 共享密钥。

幸运的是,有一个变通方法:这个技巧涉及到 Alice 生成一个新的密钥对,给予访问该密钥对的权限,然后用 Bob 的公钥加密该密钥对并分享。我们将在本章后面实际看到这一点。

设计 EMR 的 DApp 架构

让我们为使医疗应用程序能够共享数据而设计 DApp 的架构。基本上,使用不同医疗应用程序的用户可以相互分享 EMR。

该应用程序的生态系统将由医疗服务提供商(如医院、实验室和保险公司)、患者、应用程序提供商(将构建与此区块链网络集成的医疗应用程序的公司)以及网络管理机构或管理员(卫生部门和/或解决方案提供商)组成。

下图显示了高级别的架构:

这就是前述架构的工作原理:

  • 网络管理员决定谁可以加入网络并连接到云服务器。

  • 区块链将保存服务提供商和患者的身份和权限,而集中式和分布式服务器将存储加密的 EMR。每个用户和服务提供商都将拥有自己的以太坊密钥来识别自己。我们之所以不把加密的 EMR 存储在区块链中,有两个原因:

    • 每一份数据都将被复制到区块链中的每个节点。这将损害可扩展性,因为节点的大小将大幅增加。

    • 根据合规性规定,你甚至不应该分享加密数据,因为加密算法可能在未来被破解,所有数据都将泄漏。所以,如果它被保存在一个中心位置,那么服务器可以立即被拔掉。

  • 每当有人从存储中请求数据时,服务器将检查区块链以查看患者是否已授予访问权限,如果是,则将使用重新加密密钥重新加密数据,并将重新加密的数据提供给接收者。

  • 在向云服务器请求读取或写入数据时,客户端应该使用服务器提供的令牌进行签名以证明其身份。基于此,云服务器将在区块链中寻找权限并决定是否重新加密。客户端将使用他们的私有以太坊密钥进行签名。这是向云服务器进行身份验证的过程。

  • 用户将在区块链上注册其身份。如果患者想要授权其数据访问权限,那么他们将生成一个新的私钥和一个随机令牌。患者将使用这个私钥生成一个重加密密钥,并将其放到区块链上,供云服务器在重新加密数据时参考。用户无法直接使用服务提供商的密钥生成重加密密钥,因为在这种情况下,服务提供商必须暴露他们的私钥,因为我们正在使用交互式 PRE 算法。患者将会把随机令牌的哈希值添加到区块链上,而服务提供商必须发送一笔交易,证明他们知道随机令牌,作为用户分享访问权限的证明。这是一种授权数据访问的方法。例如,如果用户想要在手机上授权他们的 EMR,那么用户将生成一个包含私钥和令牌的二维码。然后他们将把交易发送到区块链,声明任何能证明他们拥有令牌的人都将被授予访问他们数据的权限。在医院,前台可以简单扫描二维码,推动一笔交易证明获取随机字符串,从而获得用户的数据访问权限。二维码也将保存私钥。每次患者想要授权某人访问时,他们都必须生成新的密钥对和令牌。

  • 一旦获得写入访问权限,服务提供商可以创建指定格式的 EMR,将其哈希值放在区块链上,然后将使用患者的公钥加密的 EMR 发送至云服务器存储。

  • 云服务器由卫生主管部门或解决方案提供商控制。它应该符合卫生主管部门的标准并遵守规范。即使云服务器被黑客攻击,黑客也无法读取任何内容,因为存储在其中的所有内容都是加密的,并且密钥分布在各个应用提供商之间。

  • 可以通过扩展这个解决方案,让最终用户控制他们的私钥,从而不必信任应用提供商。但这将损害用户体验,因为用户不习惯存储私钥。如果他们失去了密钥,那么他们对 EMR 的访问权限将永远丢失。

  • 尽管云服务器为访问 EMR 创建了集中化,但您仍然可以信任它。密钥并没有存储在云服务器中,它只是起到存储作用。即使云服务器未经您的允许将您的数据授权给他人,接收者也无法读取数据,因此可以信任云服务器。该服务器可以分布式部署以实现高可用性。

  • 这个解决方案允许您从不同的应用提供商那里使用不同的医疗保健应用,并且仍然在每个应用上看到所有的 EMR。要在新应用上导入所有 EMR,用户必须从上一个应用中导出密钥,并将其导入新的应用中。

仍然存在一个问题:在授予某人访问权限后,如何撤销访问权限?当然,您可以在区块链上撤销访问权限,但如果云服务器仍然向服务提供商提供您的新 EMR 访问权限怎么办?云服务器这样做的可能性很小,因为云服务器没有任何激励这样做。但患者可以通过更改其密钥并在其数据上运行 PRE 来避免这种情况。这将使您分享的所有重新加密密钥无效,因此以前授予访问权限的现有服务提供商无法读取新的 EMR。

身份和访问控制的智能合约

让我们编写智能合约,负责注册患者和服务提供者的身份,并提供访问控制。

这是智能合约代码:

pragma solidity ⁰.4.22;

contract Health {

    address owner;

    struct ServiceProvider {
        string publicKey;
    }

    struct Permission {
        bool read;
        bool write;
        string reEncKey; //Re-Encrypt Key
    }

    struct Token {
        int status;
        bool read;
        bool write;
        string reEncKey; //Re-Encrypt Key
    }

    struct EMR {
        string hash;
        address issuer;
    }

    struct Patient {
        string publicKey;
        mapping (address => Permission) permissions;
        mapping (bytes32 => Token) tokens;
        bool closed;
        EMR[] EMRs;
    }

    mapping (address => ServiceProvider) serviceProviders;
    mapping (address => Patient) patients;

    event tokenVerified (bytes32 hash, address patient, address
      serviceProvider);
    event reEncKeyAdded (address patient, address serviceProvider);
    event patientAccountChanged(address oldAccountAddress, string
      oldAccountPublicKey, address newAccountAddress, string 
      newAccountPublicKey, string reEncKey);
    event emrAdded(address patient, address serviceProvider, 
      string emrHash);

    constructor() {
        owner = msg.sender;
    }

    //Utilities
    function fromHexChar(uint c) public pure returns (uint) {
        if (byte(c) >= byte('0') && byte(c) <= byte('9')) {
            return c - uint(byte('0'));
        }
        if (byte(c) >= byte('a') && byte(c) <= byte('f')) {
            return 10 + c - uint(byte('a'));
        }
        if (byte(c) >= byte('A') && byte(c) <= byte('F')) {
            return 10 + c - uint(byte('A'));
        }
    }

    function fromHex(string s) public pure returns (bytes) {
        bytes memory ss = bytes(s);
        require(ss.length%2 == 0); // length must be even
        bytes memory r = new bytes(ss.length/2);
        for (uint i=0; i<ss.length/2; ++i) {
            r[i] = byte(fromHexChar(uint(ss[2*i])) * 16 +
                        fromHexChar(uint(ss[2*i+1])));
        }
        return r;
    }

    //Register Patient
    function addPatient(string publicKey) returns (int reason) {
        if(address(keccak256(fromHex(publicKey))) == msg.sender) {
            patients[msg.sender].publicKey = publicKey;
        }
    }

    //Register Service provider
    function addServiceProvider(string publicKey) {
        if(address(keccak256(fromHex(publicKey))) == msg.sender) {
            serviceProviders[msg.sender].publicKey = publicKey;
        }
    }

    //Patient:
    //In QRCode include token string, address and private key
    //Adds the hash of token and derivation key in Blockchain
    function addToken(bytes32 hash, bool read, bool write, string reEncKey) {
        if(patients[msg.sender].tokens[hash].status == 0 &&
          patients[msg.sender].closed == false) {
            patients[msg.sender].tokens[hash].status = 1;
            patients[msg.sender].tokens[hash].read = read;
            patients[msg.sender].tokens[hash].write = write;
            patients[msg.sender].tokens[hash].reEncKey = reEncKey;
        }
    }

    //Service Provider proves the token to get access
    function requestAccess(string token, address patient) {
        bytes32 hash = sha256(token);
        if(patients[patient].tokens[hash].status == 1) {
            patients[patient].tokens[hash].status = 2;
            patients[patient].permissions[msg.sender].read =
              patients[patient].tokens[hash].read;
            patients[patient].permissions[msg.sender].write =
              patients[patient].tokens[hash].write;
            patients[patient].permissions[msg.sender].reEncKey =
              patients[patient].tokens[hash].reEncKey;
            tokenVerified(hash, patient, msg.sender);
        }
    }

    //Add EMR
    function addEMR(address patient, string hash) {
        if(patients[patient].permissions[msg.sender].write == true) {
            patients[patient].EMRs.push(EMR(hash, msg.sender));
            emrAdded(patient, msg.sender, hash);
        }
    }

    function getPatientPublicKey(address patient) returns
      (string publicKey) {
        return patients[patient].publicKey;
    }

    function isPatientProfileClosed(address patient) returns 
      (bool isClosed) {
        return patients[patient].closed;
    }

    function getServiceProviderPublicKey(address serviceProvider)
      returns (string publicKey) {
        return serviceProviders[serviceProvider].publicKey;
    }

    //Revoke Access. Here you aren't changing the key.
    function revokeServiceProviderAccess(address serviceProvider) {
        patients[msg.sender].permissions[serviceProvider].read = false;
        patients[msg.sender].permissions[serviceProvider].write =
        false;
    }

    function getPermission(address patient, address serviceProvider)
      returns(bool read, bool write, string reEncKey) {
        return (patients[patient].permissions[serviceProvider].read,
          patients[patient].permissions[serviceProvider].read,
          patients[patient].permissions[serviceProvider].reEncKey);
    }

    function getToken(address patient, bytes32 hash) returns (int
      status, bool read, bool write, string reEncKey) {
        return (patients[patient].tokens[hash].status,
          patients[patient].tokens[hash].read,
          patients[patient].tokens[hash].write,
          patients[patient].tokens[hash].reEncKey);
    }

    //Change your keys to revoke old account and move EMRs to new
    // account.
    function changePatientAccount(string reEncKey, 
      address newAddress, string newPublicKey) {
        patients[msg.sender].closed = true;
        if(address(keccak256(fromHex(newPublicKey))) == newAddress) {
            patients[newAddress].publicKey = newPublicKey;
            patientAccountChanged(msg.sender,
              patients[msg.sender].publicKey, newAddress, 
              newPublicKey, reEncKey);
        }
    }
}

前述智能合约中的大部分代码都是不言自明的。在注册患者和服务提供者时,我们正在传递公钥,并验证公钥是否正确。address(keccak256(fromHex(publicKey))短语计算publicKeyaddresschangePatientAccount用于更改用户的帐户密钥,以防密钥被泄露。例如,如果您的应用提供商的服务器遭到黑客攻击并且您的私钥泄露了,应用提供商可以使用此功能来停用以前的帐户并为用户生成新帐户。云服务器将查找patientAccountChanged事件,并对加密的 EMR 运行重新加密,以便您可以使用新密钥访问它们。然后它将删除旧的加密 EMR。用户还可以使用此功能向所有服务提供商撤销对 EMR 的访问权限。

编写用于测试的 Python 和 JS 脚本

现在让我们编写一些测试脚本来测试智能合约和数据和用户流。我们将编写 Python 脚本来加密数据、解密数据、生成重新加密密钥和重新加密数据。我们将使用 Node.js 调用 Python 脚本和智能合约函数。

创建一个名为test的目录。在其中,创建一个名为encrypt.py的文件,并将以下代码放入其中:

from npre import bbs98
pre = bbs98.PRE()
import base64
import sys

publicKey = base64.b64decode(sys.argv[1])
encrypted_message = pre.encrypt(publicKey, sys.argv[2])

print(base64.b64encode(encrypted_message))

此脚本接受两个参数,publicKey 和原始消息。 publicKey 作为base64编码的公钥传递。此脚本将公钥转换为字节,以便npre库可以利用它。最后,它加密消息并将其打印为base64编码的密文。

创建另一个名为decrypt.py的文件,并将以下代码放入其中:

from npre import bbs98
pre = bbs98.PRE()
import base64
import sys

privateKey = base64.b64decode(sys.argv[1])
encrypted_message = base64.b64decode(sys.argv[2])

decrypted_message = pre.decrypt(privateKey, encrypted_message)

print(decrypted_message)

这段代码负责解密。现在,创建另一个名为generate_reEncKey.py的文件,并将以下代码放入其中:

from npre import bbs98
pre = bbs98.PRE()
import base64
import sys

base64_privateKeyA = base64.b64decode(sys.argv[1])
base64_privateKeyB = base64.b64decode(sys.argv[2])

re_ab = pre.rekey(base64_privateKeyA, base64_privateKeyB)

print(base64.b64encode(re_ab))

这段代码负责生成重新加密密钥。现在,创建另一个名为re_encrypt.py的文件,并将以下代码放入其中:

from npre import bbs98
pre = bbs98.PRE()
import base64
import sys

reEncryptKey = base64.b64decode(sys.argv[1])
encrypted_message = base64.b64decode(sys.argv[2])
re_encrypted_message = pre.reencrypt(reEncryptKey, encrypted_message)

print(base64.b64encode(re_encrypted_message))

这段代码负责重新加密密文。现在创建一个 package.json 文件,用于保存我们 Node.js 应用程序的依赖项。将以下内容放入文件中,并运行 npm install 命令来安装模块:

{
    "name": "health",
    "private": true,
    "dependencies": {
        "eth-crypto": "¹.2.1",
        "ethereumjs-tx": "~1.3.4",
        "ethereumjs-util": "~5.2.0",
        "ethereumjs-wallet": "~0.6.0",
        "sha256": "~0.2.0",
        "web3": "⁰.20.6",
        "child_process": "~1.0.2"
    }
}

现在,最后,创建一个名为 app.js 的文件,并在其中放置以下测试代码:

let Web3 = require("web3");
let ethereumjsWallet = require("ethereumjs-wallet")
let ethereumjsUtil = require("ethereumjs-util");
let ethereumjsTx = require("ethereumjs-tx");
let sha256 = require("sha256");
let EthCrypto = require('eth-crypto');
let exec = require("child_process").exec;

let web3 = new Web3(new 
  Web3.providers.HttpProvider("http://localhost:8545"));

let healthContract = web3.eth.contract([]);
let health = healthContract.new({
  from: web3.eth.accounts[0],
  data: '0x608060aa31862e....',
  gas: '4700000'
}, function(e, contract) {
  if (typeof contract.address !== 'undefined') {
    let healthContractInstance = healthContract.at(contract.address);

    //Generate Patient's Keys
    let patient_wallet = ethereumjsWallet.generate();

    //Register the Patient on blockchain.
    let data = healthContractInstance.addPatient.getData
      (patient_wallet.getPublicKey().toString('hex'));
    let nonce = web3.eth.getTransactionCount
      (patient_wallet.getAddressString())

    let rawTx = {
      gasPrice: web3.toHex(web3.eth.gasPrice),
      gasLimit: web3.toHex(4700000),
      from: patient_wallet.getAddressString(),
      nonce: web3.toHex(nonce),
      data: data,
      to: contract.address
    };

    let privateKey = ethereumjsUtil.toBuffer("0x" +
      patient_wallet.getPrivateKey().toString('hex'), 'hex');
    let tx = new ethereumjsTx(rawTx);
    tx.sign(privateKey);

    web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'),
      function(error, result) {
      if (error) {
        console.log(error)
        res.status(500).send({
          error: "An error occured"
        })
      } else {
        console.log("Patient Pub Key: " +  
          healthContractInstance.getPatientPublicKey.call
          (patient_wallet.getAddressString()))

        //Generate Service Provider's Keys
        let hospital_wallet = ethereumjsWallet.generate();

        //continue from here
      }
    })
  }
})

编译智能合约,并将 ABI 和字节码分别填充到 healthContracthealth 变量中。

这是前面代码的工作原理:

  • 我们使用 ethereumjs 库来创建离线账户,并使用这些账户进行交易签名。

  • 我们使用 child_process 来从 Node.js 执行 Python 脚本。尽管您可以使用 RESTful API 并采用微服务架构,但出于测试目的,这样做是可以的。

  • 我们使用 EthCrypto 来压缩和解压缩公钥。由 ethereumjs-wallet 生成的公钥是未压缩的,而由 npre 生成和使用的公钥是压缩的。私钥始终为 32 字节,公钥始终为 65 字节(或者压缩公钥为 33 字节)。公钥哈希始终为 20 字节。 npre 还在私钥的开头添加了 0x00,在公钥的开头添加了 0x01

  • 首先,我们生成一个患者的钱包,并将其注册到区块链上。在真实应用程序中,您还可以在云服务器上注册用户配置文件和服务提供商。用户配置文件可以包含患者的姓名、年龄和其他详细信息;同样,服务提供商配置文件可以包含许可证号码、名称等。这些配置文件可以使用所有者的公钥加密并存储在云服务器上。

现在,在我们有一个续行注释的地方插入以下代码:

//Generate Service Provider's Keys
let hospital_wallet = ethereumjsWallet.generate();

//Register the Service Provider on blockchain
let data = healthContractInstance.addServiceProvider.getData
  (hospital_wallet.getPublicKey().toString('hex'));
let nonce = web3.eth.getTransactionCount
  (hospital_wallet.getAddressString())

let rawTx = {
  gasPrice: web3.toHex(web3.eth.gasPrice),
  gasLimit: web3.toHex(4700000),
  from: hospital_wallet.getAddressString(),
  nonce: web3.toHex(nonce),
  data: data,
  to: contract.address
};

let privateKey = ethereumjsUtil.toBuffer("0x" +
   hospital_wallet.getPrivateKey().toString('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);

web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'),
  function(error, result) {
  if (error) {
    console.log(error)
  } else {
    console.log("Hospital Pub Key: " +
      healthContractInstance.getServiceProviderPublicKey.call
      (hospital_wallet.getAddressString()))

    let token = "yr238932";
    let tokenHash = "0x" + sha256(token);

    //Generate private key like npre. It has a extra character 0x00 
    //in beginning
    let secKeyA = Buffer.concat([new Buffer([0x00]),
      patient_wallet.getPrivateKey()]).toString('base64')
    //Generate another private key to share with service provider
    let temp_wallet = ethereumjsWallet.generate();
    let secKeyB = Buffer.concat([new Buffer([0x00]),
      temp_wallet.getPrivateKey()]).toString('base64')

    exec('python3 ./generate_reEncKey.py ' + secKeyA + " " + secKeyB,
      (error, stdout, stderr) => {
      if (error !== null) {
        console.log(error)
      } else {
        let reEncKey = stdout.substr(2).slice(0, -2)

        console.log("Re-Encryption Key: " + reEncKey)

        //Add token to blockchain
        let data = healthContractInstance.addToken.getData
          (tokenHash, true, true, reEncKey);
        let nonce = web3.eth.getTransactionCount
          (patient_wallet.getAddressString())

        let rawTx = {
          gasPrice: web3.toHex(web3.eth.gasPrice),
          gasLimit: web3.toHex(4700000),
          from: patient_wallet.getAddressString(),
          nonce: web3.toHex(nonce),
          data: data,
          to: contract.address
        };

        let privateKey = ethereumjsUtil.toBuffer("0x" + 
          patient_wallet.getPrivateKey().toString('hex'), 'hex');
        let tx = new ethereumjsTx(rawTx);
        tx.sign(privateKey);

        web3.eth.sendRawTransaction("0x" + 
          tx.serialize().toString('hex'), 
          function(error, result) {
          if (error) {
            console.log(error)
          } else {
            console.log("Token Info: " + 
              healthContractInstance.getToken.call
              (patient_wallet.getAddressString(), tokenHash, {
              from: patient_wallet.getAddressString()
            }))

            //Get access to patient's data
            let data = 
              healthContractInstance.requestAccess.getData
              (token, patient_wallet.getAddressString());
            let nonce = web3.eth.getTransactionCount
              (hospital_wallet.getAddressString())

            let rawTx = {
              gasPrice: web3.toHex(web3.eth.gasPrice),
              gasLimit: web3.toHex(4700000),
              from: hospital_wallet.getAddressString(),
              nonce: web3.toHex(nonce),
              data: data,
              to: contract.address
            };

            let privateKey = ethereumjsUtil.toBuffer("0x" +
              hospital_wallet.getPrivateKey().toString('hex'),
              'hex');
            let tx = new ethereumjsTx(rawTx);
            tx.sign(privateKey);

            web3.eth.sendRawTransaction("0x" + 
              tx.serialize().toString('hex'), 
              function(error, result) {
              if (error) {
                console.log(error)
              } else {
                console.log("Permission Info: " +
                  healthContractInstance.getPermission.call
                  (patient_wallet.getAddressString(), 
                  hospital_wallet.getAddressString(), {
                  from: hospital_wallet.getAddressString()
                }))

              }
            })
          }
        })
      }
    })
  }
})

在这里,我们生成了一个临时密钥对,并假设它与服务提供商共享。然后,我们使用患者的私钥和临时私钥生成了一个重新加密密钥。然后,我们从患者的钱包执行了一个 addToken 交易和一个从服务提供商的钱包执行了一个 requestAccess 交易。这两个交易为服务提供商提供了访问患者 EMR 的权限。

现在,在我们有一个续行注释的地方插入以下代码:

let emr = JSON.stringify({
  "Blood Group": "O+",
  "type": "Blood Report"
});
let emrHash = sha256(emr);

let data = healthContractInstance.addEMR.getData
  (patient_wallet.getAddressString(), emrHash);
let nonce = web3.eth.getTransactionCount
  (hospital_wallet.getAddressString())

let rawTx = {
  gasPrice: web3.toHex(web3.eth.gasPrice),
  gasLimit: web3.toHex(4700000),
  from: hospital_wallet.getAddressString(),
  nonce: web3.toHex(nonce),
  data: data,
  to: contract.address
};

let privateKey = ethereumjsUtil.toBuffer("0x" + hospital_wallet.getPrivateKey().toString('hex'), 'hex');
let tx = new ethereumjsTx(rawTx);
tx.sign(privateKey);

web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'),
   function(error, result) {
  if (error) {
    console.log(error)
  } else {
    //Generate Public Key like npre. It's compressed and has a 
    //extra character 0x01 in beginning
    let compressedPublicKey = Buffer.concat
      ([new Buffer([0x01]), Buffer.from(EthCrypto.publicKey.compress
      (patient_wallet.getPublicKey().toString("hex")),
      'hex')]).toString("base64")

    exec('python3 ./encrypt.py ' + compressedPublicKey + " '" + 
      emr + "'", (error, stdout, stderr) => {
      if (error !== null) {
        console.log(error)
      } else {
        //Assume we are pushing encrypted data to proxy 
        //re-encryption server
        let encryptedEMR = stdout.substr(2).slice(0, -2);
        console.log("Encrypted Message: " + encryptedEMR)

        //Assume that proxy re-encryption server re-encrypting
        // data when requested by authorized service provider
        exec('python3 ./re_encrypt.py ' + reEncKey + " " +
          encryptedEMR, (error, stdout, stderr) => {
          if (error !== null) {
            console.log(error)
          } else {
            let reEncryptedEMR = stdout.substr(2).slice(0, -2)
            console.log("Re-Encrypted Message: " + reEncryptedEMR)

            //Assume service provider decrypting the re-encrypted 
            //data provided by the proxy re-encryption server
            exec('python3 ./decrypt.py ' + secKeyB + " " +
              reEncryptedEMR, (error, stdout, stderr) => {
              if (error) {
                console.log(error)
              } else {
                let decrypted_message = stdout.substr(2).slice(0, -2)

                console.log("Decrypted Message: " + decrypted_message)

                //Generate a new key for patient
                let new_patient_wallet = ethereumjsWallet.generate();

                let secKeyA = Buffer.concat([new Buffer([0x00]),
                  patient_wallet.getPrivateKey()]).toString('base64')
                let secKeyB = Buffer.concat
                  ([new Buffer([0x00]),
                   new_patient_wallet.getPrivateKey()]
                   ).toString('base64')

                exec('python3 ./generate_reEncKey.py ' + secKeyA + " "
                  + secKeyB, (error, stdout, stderr) => {
                  if (error !== null) {
                    console.log(error)
                  } else {
                    let reEncKey = stdout.substr(2).slice(0, -2)

                    console.log("Re-encryption Key for Patient's new
                      Wallet: " + reEncKey)

                    //Change patient's key
                    let data = healthContractInstance.
                      changePatientAccount.getData
                      (reEncKey, new_patient_wallet.getAddressString(),
                      new_patient_wallet.getPublicKey().
                      toString('hex'));
                    let nonce = web3.eth.getTransactionCount
                      (patient_wallet.getAddressString())

                    let rawTx = {
                      gasPrice: web3.toHex(web3.eth.gasPrice),
                      gasLimit: web3.toHex(4700000),
                      from: patient_wallet.getAddressString(),
                      nonce: web3.toHex(nonce),
                      data: data,
                      to: contract.address
                    };

                    let privateKey = ethereumjsUtil.toBuffer("0x" +
                      patient_wallet.getPrivateKey().toString
                      ('hex'), 'hex');
                    let tx = new ethereumjsTx(rawTx);
                    tx.sign(privateKey);

                    web3.eth.sendRawTransaction("0x" +
                      tx.serialize().toString('hex'), 
                      function(error, result) {
                      if (error) {
                        console.log(error)
                      } else {
                        let events = healthContractInstance.allEvents({
                          fromBlock: 0,
                          toBlock: 'latest'
                        });
                        events.get(function(error, logs) {
                          for (let count = 0; count < logs.length;
                            count++) {
                            console.log("Event Name: " +
                              logs[count].event + " and Args: " +
                              JSON.stringify(logs[count].args))
                          }
                        });
                      }
                    })
                  }
                })
              }
            })
          }
        })
      }
    });
  }
})

在这里,我们创建了一个表示血型的样本 EMR。然后我们将哈希放在区块链上,并假设将加密的 EMR 放在了云服务器上。然后,我们模拟了一个场景,云服务器重新加密了密文,服务提供商解密了密文。最后,我们生成了另一对密钥,并将患者的所有 EMR 移动到该账户,并关闭了旧账户。

所以,你看到了我们是如何模拟整个用户流程的,以及你如何使用 PRE 来确保安全性和隐私性。

概要

在这一章中,我们学习了如何使用 PRE 在区块链中实现加密数据共享。在许多情况下,PRE 可以成为私有交易和 ZSL 的良好替代方案。我们所看到的架构可以应用于许多其他情况,其中敏感资产需要在对等方之间存储和共享。

除了 PRE 外,我们还了解了许多 JS 和 Python 库,比如etherumjs-walletethereumjs-txethereumjs-utilnpre。我们还学习了如何发送原始交易,比如使用存储在 geth 节点外部的密钥签署交易的过程。在下一章中,我们将学习如何在 Quorum 中实现网络权限管理,以及如何使用手机号码构建转账解决方案。

第八章:为银行建立支付解决方案

如今,有许多由银行和其他金融科技公司开发的应用程序和服务,让我们可以发送和接受付款。但是我们还没有一个应用程序能够使发送和接收资金像发送和接收短信一样简单。虽然比特币和其他加密货币使得全球范围内的付款变得非常简单,但由于波动性和监管问题,它们目前无法成为主流。在本章中,我们将建立一个 P2P 支付系统,使发送和接收银行间支付变得非常容易,并且在银行之间的结算和清算几乎实时和简单。在构建解决方案的同时,我们还将学习各种银行和金融概念。

在本章中,我们将学习以下内容:

  • 银行间国内和国际电子转账的清算和结算方式

  • 全球银行间金融电信协会SWIFT)系统及银行间国际汇款的工作原理

  • 如何在区块链上数字化法定货币以及它解决的问题

  • 如何在 Quorum 中实现网络权限管理

  • 如何构建使用手机号码转账的解决方案

支付系统概述

在本章中,我们将建立一个可集成在手机银行应用程序中的支付解决方案。这个解决方案将允许客户使用手机号码发送付款。只需使用手机号码就可以向世界上任何人发送付款将是发送付款的最友好方式。

我们的解决方案将使用数字化法定货币来进行银行间转账的结算和清算。为了理解为什么我们选择使用数字化法定货币作为结算媒介,让我们先了解一下银行间转账的结算和清算方式及其问题。

银行间转账的结算与清算

让我们首先了解国内银行间转账的工作原理。每个国家的中央银行都有一种或多种不同类型的集中式电子资金转移系统。例如,印度的即时付款服务IMPS),美国的自动清算机构ACH),加拿大的电子资金转账EFT)。这些系统被各国银行用来向彼此发送消息,以促进向其客户的资金转移。只有消息被转移,而不是真正的资金。最终的结算通过结算账户进行。每家银行在中央银行都持有一个结算账户,当有转账消息时,资金要么在这些账户中存入,要么支出。为了更清楚地理解这一点,让我们看一个例子。

假设银行A在中央银行有一个结算账户,其中存入了$50,000。同样,假设银行B在中央银行有一个结算账户,其中包含$100,000。现在,假设X是银行A的客户,Y是银行B的客户。当X想向Y发送$100 时,银行A通过资金转移系统向银行B发送一条消息,指示已从X的账户中扣除$100,并将$100 存入银行BY账户中。收到该消息后,银行B立即将Y的账户存入$100。为了结算这笔款项,中央银行从银行A的结算账户中扣除$100,因此新余额为$49,900,并将$100 存入银行B的结算账户,因此新余额为$100,100。

中央银行通常在特定时间每天进行最终结算。消息传输几乎是实时的。只要涉及的两家银行都信任中央银行,这个过程就能正常运作。

让我们看看国际银行间转账是如何运作的。在这种情况下,涉及两个不同国家的两家银行,国际支付的方式与国内转账不同。在国际支付的情况下,银行使用 SWIFT 系统发送消息。SWIFT 是一种金融机构用于安全传输信息和指令的消息网络,通过标准化的代码系统进行传输。SWIFT 为每个金融组织分配一个唯一的代码,代码有 8 个或 11 个字符。该代码通常称为银行识别代码BIC)、SWIFT 代码、SWIFT ID 或 ISO 9362 代码。

要了解更多关于 SWIFT 的信息,请访问www.investopedia.com/articles/personal-finance/050515/how-swift-system-works.asp

在这种情况下,两家银行不是与特定的中央银行拥有结算账户,而是彼此之间拥有结算账户。为了进一步理解这一点,让我们举个例子。假设银行A是一家美国银行,银行B是一家印度银行。X是银行A的客户,Y是银行B的客户。为了让X能向Y转账,反之亦然,银行A和银行B彼此之间持有结算账户。因此,银行A可能在银行B开设一个账户,其中存入了₹300,000,而银行B也会在银行A开设一个结算账户,其中存入了$100,000。现在,当XY发送价值$100 的付款时,银行B将从其管理的银行A的结算账户中扣除₹6909.50(本书编写时的汇率为 1 美元=69.10 印度卢比)。X的账户将被扣除$100,Y的账户将被存入₹6909.50。

通常需要五到七天才能反映在Y的账户中。这是由于许多必要的流程、检查和问题,比如以下内容:

  • 由于外汇汇款促成了洗钱行业,银行必须进行某种背景检查,以确保您使用的资金不是来自非法来源。

  • 汇款人的银行在其系统中搜索,查看是否与接收人账户所在的银行直接合作(持有结算账户)。通常情况下,这种情况不太可能发生。因此,汇款人的银行与他们有合作关系的银行联系,同时也知道接收方银行也是他们的合作伙伴。因此,这基本上形成了一个链条。有时,这个链条可能会在中间延伸到三四家银行,这取决于您所在国家的银行基础设施和经济开放程度。

  • 大多数情况下,这些 SWIFT 消息只由汇款人银行的一个专门从事外汇汇款的分支机构发送。此外,汇款人分支机构会花费更多时间将消息发送到主分支机构,然后再由那里继续进行。

  • 在许多国家,与法规和合规相关的支票仍需手动处理,从而增加了转账完成时间。

存放款项账户(Nostro)是银行A用来指代由银行B持有的我们的账户的术语。您账户(Vostro)是银行 B 使用的术语,其中存放着银行 A 的资金。

数字化法定货币

我们看到了银行间转账是如何运作的。对于国内转账,中央银行必须负责管理和更新结算账户,而对于国际转账,各银行必须努力更新结算账户。在国际转账的情况下,还存在其他问题,例如需要更多的对账工作,因为没有可信第三方,还有通过多个中介银行进行支付的路由。

区块链使银行能够通过提供数字化法定货币的能力直接将资金转移到世界上的任何其他银行;通过提供单一真相源,它减少了大量对账工作。

让我们来看看数字化法定货币在区块链上的过程和流程:

  • 只有中央银行有权在区块链上发行各自的数字化法定货币。

  • 我们可以为每种法定货币建立一个单独的网络,而不是使用单一网络分配流量并增加可扩展性。

  • 要将法定货币转换为数字化形式,银行必须将法定货币存入中央银行的现金保管账户。中央银行将在区块链上向各银行发行同等金额的数字化法定货币。

  • 银行可以随时通过在区块链上销毁数字化法定货币来将数字化法定货币转换回纸币。

  • 为了实现匿名性,银行可以使用多个地址。因此,其他银行将难以预测谁拥有多少数字化法定货币。

使用手机号作为身份证明

我们的支付应用将基于使用手机号码作为接收方的身份。让我们看看使用区块链将手机号码作为支付标识符的整个过程:

  • 区块链将充当与银行代码相关联的手机号码的共享和受保护的存储。

  • 每个 ISD 代码将有自己的网络。这是出于可扩展性的考量。

  • 每个手机号码可以与一个或多个银行相关联。如果有多个银行,则发送方可以选择要发送支付的银行账户。

  • 接收方要使用手机号码接收支付,必须通过接收银行的手机银行应用在区块链上注册手机号码。

  • 如果接收方的银行账户被暂停,应更新区块链上的状态,以通知其他人不要接受该手机号的支付。

构建网络

在我们继续编写智能合约之前,让我们为+1 ISD 代码的美元货币创建 Quorum 网络。我们将确保这些网络是经过许可的,并使用节点 ID 进行保护。

到目前为止,对于我们在本书中创建的所有网络,我们已经假设它们是使用白名单 IP 进行保护的。但是夸罗姆提供了一种方式来对节点 ID 进行白名单设置。你可以将相同的实践应用于本书中构建的其他网络。手机号码不应泄露到网络之外,因此重要性在于不惜一切代价保护网络。

Quorum 中的网络许可

网络许可是通过在节点启动期间将--permissioned标志作为命令行参数添加到各个节点级别启用的。当添加该标志时,节点将在节点的数据目录文件夹中寻找名为permissioned-nodes.json的文件。

permissioned-nodes.json文件包含了该特定节点将接受来自和向外部连接的节点标识符(enode://nodeID@ip:port)的列表。

如果设置了--permissioned标志,但permissioned-nodes.json文件为空或根本不存在于节点的数据目录文件夹中,则节点将启动,但它既不会连接到任何其他节点,也不会接受来自其他节点的任何传入连接请求。

例如,在我们的案例中,我们需要至少三个节点网络,即 A 银行,B 银行和中央银行。假设 A 银行的节点 ID 是480cd6ab5c7910af0e413e17135d494d9a6b74c9d67692b0611e4eefea1cd082adbdaa4c22467c583fb881e30fda415f0f84cfea7ddd7df45e1e7499ad3c680c,B 银行的节点 ID 是60998b26d4a1ecbb29eff66c428c73f02e2b8a2936c4bbb46581ef59b2678b7023d300a31b899a7d82cae3cbb6f394de80d07820e0689b505c99920803d5029a以及中央银行的节点 ID 是e03f30b25c1739d203dd85e2dcc0ad79d53fa776034074134ec2bf128e609a0521f35ed341edd12e43e436f08620ea68d39c05f63281772b4cce15b21d27941e

因此,Bank A 节点上的permissioned-nodes.json文件将包含以下内容:

[
  "enode://60998b26d4a1ecbb29eff66c428c73f02e2b8a2936c4bbb46581ef59b2678b7023d300a31b899a7d82cae3cbb6f394de80d07820e0689b505c99920803d5029a@[::]:23001?discport=0",
  "enode://e03f30b25c1739d203dd85e2dcc0ad79d53fa776034074134ec2bf128e609a0521f35ed341edd12e43e436f08620ea68d39c05f63281772b4cce15b21d27941e@[::]:23002?discport=0"
]

类似地,银行B将白名单中的银行A和央行,而央行将白名单中的银行A和银行B

permissioned-nodes.json文件的任何添加都将在后续的传入/传出请求时动态地被服务器接收。节点不需要重新启动以使更改生效,但是从permissioned-nodes.json文件中删除现有连接的节点不会立即断开这些现有连接的节点。但是,如果出于任何原因断开了连接,并且从已断开的节点 ID 发出了后续的连接请求,那么该请求将作为该新请求的一部分被拒绝。

构建 DApp

让我们编写智能合约,将法定货币数字化并存储与银行账户关联的手机号。这是用于数字化法定货币的智能合约:

pragma solidity ⁰.4.18;

contract USD {
    address centralBank;

    mapping (address => uint256) balances;
    uint256 totalDestroyed;
    uint256 totalIssued;

    event usdIssued(uint256 amount, address to);
    event usdDestroyed(uint256 amount, address from);
    event usdTransferred(uint256 amount, address from, address to,
      string description);

    function USD() {
        centralBank = msg.sender;
    }

    function issueUSD(uint256 amount, address to) {
        if(msg.sender == centralBank) {
            balances[to] += amount; 
            totalIssued += amount;
            usdIssued(amount, to);
        }
    }

    function destroyUSD(uint256 amount) {
        balances[msg.sender] -= amount;
        totalDestroyed += amount;
        usdDestroyed(amount, msg.sender);
    }

    function transferUSD(uint256 amount, address to, string 
      description) {
        if(balances[msg.sender] >= amount) {
            balances[msg.sender] -= amount;
            balances[to] += amount;
            usdTransferred(amount, msg.sender, to, description);
        }
    }

    function getBalance(address account) returns (uint256 balance) {
        return balances[account];
    }

    function getTotal() returns (uint256 totalDestroyed, uint256 
      totalIssued) {
        return (totalDestroyed, totalIssued);
    }
}

这是上述代码的工作原理:

  • 我们假设央行部署了智能合约。

  • 我们有方法来发行、转移和销毁USD。这些方法都是不言自明的。转移方法还有一个描述,可以包含诸如交易目的或接收客户详细信息等信息。

  • 我们有方法来检索账户的余额,以及发行和销毁的总USD

以下是保存手机号及其对应银行的智能合约:

pragma solidity ⁰.4.18;

contract MobileNumbers {
    address centralBank;

    struct BankDetails {
        string name;
        bool authorization;
    }

    mapping (address => BankDetails) banks;
    mapping (uint256 => address[]) mobileNumbers;

    event bankAdded(address bankAddress, string bankName);
    event bankRemoved(address bankAddress);
    event mobileNumberAdded(address bankAddress, uint256 mobileNumber);

    function MobileNumbers() {
        centralBank = msg.sender; 
    }

    function addBank(address bank, string bankName) {
        if(centralBank == msg.sender) {
            banks[bank] = BankDetails(bankName, true);
            bankAdded(bank, bankName);
        }
    }

    function removeBank(address bank) {
        if(centralBank == msg.sender) {
            banks[bank].authorization = false;
            bankRemoved(bank);
        } 
    }

    function getBankDetails(address bank) view returns (string 
      bankName, bool authorization) {
        return (banks[bank].name, banks[bank].authorization);
    }

    function addMobileNumber(uint256 mobileNumber) {
        if(banks[msg.sender].authorization == true) {
            for(uint256 count = 0; count < 
              mobileNumbers[mobileNumber].length; count++) {
                if(mobileNumbers[mobileNumber][count] == msg.sender) {
                    return;
                }
            }

            mobileNumbers[mobileNumber].push(msg.sender);
            mobileNumberAdded(msg.sender, mobileNumber);
        }
    }

    function removeMobileNumber(uint256 mobileNumber) {
        if(banks[msg.sender].authorization == true) {
            for(uint256 count = 0; count < 
              mobileNumbers[mobileNumber].length; count++) {
                if(mobileNumbers[mobileNumber][count] == msg.sender) {
                    delete mobileNumbers[mobileNumber][count];

                    //fill the gap caused by delete
                    for (uint i = count; i < 
                      mobileNumbers[mobileNumber].length - 1; i++){
                        mobileNumbers[mobileNumber][i] = 
                          mobileNumbers[mobileNumber][i+1];
                    }
                    mobileNumbers[mobileNumber].length--;

                    break;
                }
            }
        }
    }

    function getMobileNumberBanks(uint256 mobileNumber) view returns
     (address[] banks) {
        return mobileNumbers[mobileNumber];
    }
}

以下是上述代码的工作原理:

  • 我们假设央行部署了该合约。

  • 然后央行可以添加或删除银行到网络中。每家银行在区块链上都有一个账户。我们不能使用任何账户编写手机号,因为这将让银行进行欺诈——即使他们没有拥有与手机号相关联的账户,他们仍然会添加它并且不会被发现。预定义的账户将实现审计,以便银行不会接受他们不持有的手机号账户的付款。

  • 然后我们有一个添加手机号的方法。每个手机号都与用户拥有账户的很多银行相关联。在发送付款时,付款人可以选择其中一个账户。

  • 然后我们有一个方法,银行可以使用它将自己从手机号中移除。当银行账户被暂停或关闭时,这是有用的。

  • 最后,我们有一个函数可以获取与手机号关联的银行账户列表。

现在让我们看看用户支付和结算的整个流程:

  • 假设X在银行A有一个账户,Y在银行B有一个账户。两家银行都在两个网络上注册,并且两家银行在USD网络上有足够的USD

  • 为了使用手机号接收付款,Y必须使用银行B的手机号在MobileNumbers网络上注册其手机号。银行B将调用addMobileNumber方法在网络上注册Y的银行账户。

  • 要让 XY 发送付款,X 必须在银行 A 的手机银行应用程序中输入 Y 的手机号码。之后,银行 A 将调用 getMobileNumberBanks 方法以获取 Y 拥有账户的银行列表。银行 B 必然会被列出,所以 X 可以选择它并点击“发送付款”按钮。

  • 一旦点击“发送付款”按钮,银行 A 将调用 transferUSD 方法,并在说明中提供 Y 的手机号码,表示要将资金存入的银行账户。transferUSD 中的 to 地址将是由 getMobileNumberBanks 方法返回的地址。

摘要

在本章中,我们学习了一些银行业的基本概念,以及银行间转账是如何结算和清算的。我们还了解了 SWIFT 以及它的工作原理。然后我们深入了解了 Quorum 中的高级网络许可,并学习了 --permissioned 标志。

最后,我们建立了一种新型的资金转移系统,使用数字化的法定货币和客户识别的手机号进行支付结算。我们将整个解决方案构建在区块链上,这样可以最大程度地减少调节工作量,并解决了以前无法解决的许多问题。

标签:function,etp,bc,我们,merge,使用,web3,区块,节点
From: https://www.cnblogs.com/apachecn/p/18169371

相关文章

  • bc-dev-hplg-merge-0
    Hyperkedge区块链开发教程(全)原文:zh.annas-archive.org/md5/7f932e9670331dae388d1a76f72881d8译者:飞龙协议:CCBY-NC-SA4.0前言这个学习路径是你探索和构建使用以太坊、HyperledgerFabric和HyperledgerComposer的区块链网络的简易参考。它从区块链的概述开始,向您展示......
  • adv-bc-dev-merge-0
    区块链高级开发教程(全)原文:zh.annas-archive.org/md5/64e2728fdd6fa177d97883a45d7dec42译者:飞龙协议:CCBY-NC-SA4.0前言区块链技术是一种分布式分类账,应用于金融、政府和媒体等行业。本学习路径是您构建使用以太坊、JavaScript和Solidity构建区块链网络的指南。您将首......
  • eth-proj-bg-merge-0
    面向初学者的ETH项目(全)原文:zh.annas-archive.org/md5/5b197ea4ae8836b6854907e58ea8a1dc译者:飞龙协议:CCBY-NC-SA4.0前言本书旨在让你深入了解以太坊区块链世界,并让你使用以太坊制作自己的加密货币。在本书中,你将学习各种概念,并直接应用这些知识,同时还将介绍以太坊区块......
  • 简单解决version 'GLIBC_2.34' not found,version 'GLIBC_2.25' not found
    简单解决version'GLIBC_2.34'notfound,version'GLIBC_2.25'notfound无需手动下载安装包编译前言很多博客都是要手动下载安装包进行编译升级,但这样很容易导致系统崩溃,本博文提供一个简单的方法,参考自博客1,博客2.检查版本strings/usr/lib64/libc.so.6|grepGLIBC_或者......
  • ABC351F
    F-DoubleSum题意简述Justit.思路1发现很像求正序对,但是需要具体数字计算。只考虑\(A_j-A_i>0\),那么我们把\(A_j,-A_i\)分开计算。考虑\(A_j\)被计算的清形,其实就是以它结尾的正序对个数。考虑\(-A_i\)被计算的清形,其实就是以它开头的正序对个数,翻转序列,转化为以......
  • ABC351E
    E-JumpDistanceSum题意简述Justit.思路兔子斜着走->国际象棋里的象->黑象只能到达黑格,白象只能到达白格(横纵坐标相加的奇偶性)。将点分成两组,则每组内的点之间都有答案。可以发现可以先朝着那个方向斜着走,然后超出的部分向着那个方向迂回是最优的。如图不难发现距离是......
  • ojdbc6jar包手动解压导入本地仓库
    报错Cannotresolvecom.oracle:ojdbc6:11.2.0.1.0<!--oracle驱动--><dependency><groupId>com.oracle</groupId><artifactId>ojdbc6</artifactId><version>11.2.0.1.0</version></dependency>解决方式:......
  • ABC350
    Alink把最后三位取成数字,判断是否小于\(349\),大于\(1\),不等于\(316\)。点击查看代码#include<bits/stdc++.h>usingnamespacestd;chars[10];intans;signedmain(){ cin>>s+1; ans+=s[4]-48; ans*=10; ans+=s[5]-48; ans*=10; ans+=s[6]-48; ......
  • ABC351讲解
    ABC351A:题意思路:直接按题意模拟,求出\(\SigmaA\)和\(\SigmaB\)再相减便是差,因为要获胜所以再\(+1\)即可。代码B:题意思路:直接按照题意\(N^2\)枚举即可。代码C:题意思路:直接按照题意模拟即可。代码D:请lrx讲解。F:题意思路:题意十分简单,就是求\(\Sig......
  • WPF SetProperty to implement compare,assign and notify
    protectedvoidSetProperty<T>(refTfield,Tvalue,[CallerMemberName]stringpropName=null){if(!EqualityComparer<T>.Default.Equals(field,value)){field=value;varhandler=PropertyChanged;if(handler!=nul......