前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面向企业的区块链教程(一)

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

作者头像
ApacheCN_飞龙
发布2024-05-24 17:13:23
1790
发布2024-05-24 17:13:23
举报
文章被收录于专栏:信数据得永生

原文: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。”

代码块设置如下:

代码语言:javascript
复制
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"

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

代码语言:javascript
复制
git clone https://github.com/jpmorganchase/quorum.git
cd quorum
make all

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

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

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

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

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

代码语言:javascript
复制
git clone https://github.com/jpmorganchase/quorum.git
cd quorum
make all

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

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

代码语言:javascript
复制
apt-get install libdb-dev libleveldb-dev libsodium-dev zlib1g-dev libtinfo-dev
curl -sSL https://get.haskellstack.org/ | sh
stack setup  

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

代码语言:javascript
复制
brew install berkeley-db leveldb libsodium
brew install haskell-stack
stack setup

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

代码语言:javascript
复制
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目录中运行以下命令来创建密钥对:

代码语言:javascript
复制
./constellation-node --generatekeys=node1
./constellation-node --generatekeys=node2
./constellation-node --generatekeys=node3
./constellation-node --generatekeys=node4

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

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

以下是constellation1.conf的代码:

代码语言:javascript
复制
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的代码:

代码语言:javascript
复制
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的代码:

代码语言:javascript
复制
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的代码:

代码语言:javascript
复制
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 节点:

代码语言:javascript
复制
./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 相关联的私钥称为节点密钥

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

代码语言:javascript
复制
./bootnode -genkey enode_id_1
./bootnode -genkey enode_id_2
./bootnode -genkey enode_id_3
./bootnode -genkey enode_id_4

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

代码语言:javascript
复制
./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:

代码语言:javascript
复制
[
 "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 中,这个文件用于达成共识,不同于以太坊中的用途是节点发现。

创建一个账户

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

代码语言:javascript
复制
./geth --datadir ./accounts account new

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

创建创世块

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

代码语言:javascript
复制
{
     "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 密钥。

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

代码语言:javascript
复制
#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 窗口中运行每个命令:

代码语言:javascript
复制
#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的交互式控制台,请使用以下命令:

代码语言:javascript
复制
./geth attach ipc:./qdata/node1/geth.ipc

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

动态添加或移除节点

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

代码语言:javascript
复制
#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文件,因为我们是动态添加它。 现在,从第四个节点的交互式控制台中,运行以下行代码将第四个对等体添加到网络中:

代码语言:javascript
复制
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。 在启动第四个节点时,您将需要这个数字。 现在,使用以下命令启动第四个节点:

代码语言:javascript
复制
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 的交互式控制台中,运行以下代码从网络中移除第三个节点:

代码语言:javascript
复制
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 工具的步骤:

代码语言:javascript
复制
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 文件。

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

代码语言:javascript
复制
./istanbul setup --num 4 --nodes --verbose

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

代码语言:javascript
复制
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

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

代码语言:javascript
复制
./geth --datadir ./accounts account new

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

代码语言:javascript
复制
{
     "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 密钥,并使用创世块引导区块链。

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

代码语言:javascript
复制
#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 窗口中运行每个命令:

代码语言:javascript
复制
./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。

动态添加或移除验证者

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

代码语言:javascript
复制
./istanbul setup --num 1 --nodes --verbose

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

代码语言:javascript
复制
#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) 其他验证者同意插入新的验证者了。为此,在所有其他验证者中运行以下命令:

代码语言:javascript
复制
istanbul.propose("0x349ec6eefe8453a875c4905f5581ea792806a3e5", true)

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

代码语言:javascript
复制
./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) 个验证者中运行以下命令以从网络中移除第一个验证者:

代码语言:javascript
复制
istanbul.propose("0x05a6245732c2350ba2ed64e840394c2239f8ad1f", false)

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

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

概要

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

第三章:编写智能合约

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

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

  • Solidity 源文件的布局
  • 理解 Solidity 数据类型
  • 特殊变量和合约函数
  • 控制结构
  • 合约的结构和特性
  • 编译和部署合约

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

Solidity 源文件

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

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

代码语言:javascript
复制
pragma Solidity ⁰.4.17;

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

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

智能合约的结构

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

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

代码语言:javascript
复制
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

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

代码语言:javascript
复制
 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 语法的示例:

代码语言:javascript
复制
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 语法的示例:

代码语言:javascript
复制
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 语法的示例:

代码语言:javascript
复制
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 的示例:

代码语言:javascript
复制
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 运算符的工作原理,如下所示:

代码语言:javascript
复制
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 还支持显式转换。如果编译器不允许两种数据类型之间的隐式转换,您可以选择显式转换。我们建议避免显式转换,因为它可能会给您带来意外的结果。

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

代码语言:javascript
复制
uint32 a = 0x12345678; 
uint16 b = uint16(a); // b will be 0x5678 now 

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

使用 var

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

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

代码语言:javascript
复制
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?:控制结构。

这里有一个结构:

代码语言:javascript
复制
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关键字创建新的合同。必须了解被创建合同的完整代码。

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

代码语言:javascript
复制
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 中抛出异常的不同方式:

代码语言:javascript
复制
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 有两种类型的函数调用:内部和外部。内部函数调用是指函数调用同一合同中的另一个函数。外部函数调用是指函数调用另一个合同的函数。

以下是一个示例:

代码语言:javascript
复制
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:私有函数和状态变量与内部函数类似,只是不能被继承合约访问。

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

代码语言:javascript
复制
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; 
    } 
} 

函数修饰符

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

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

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

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

代码语言:javascript
复制
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()的执行方式:

代码语言:javascript
复制
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 气体),因此将回退函数尽可能地廉价是很重要的。

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

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

代码语言:javascript
复制
contract sample 
{ 
    function() payable 
    { 
        //Note how much Ether has been sent and by whom 
    } 
} 

继承

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

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

代码语言:javascript
复制
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 关键字用于引用最终继承链中的下一个合约。以下是一个例子,帮助你更好地理解:

代码语言:javascript
复制
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。继承链以最派生的合约开始,以最不派生的合约结束。

抽象合约

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

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

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

代码语言:javascript
复制
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 编译器需要源代码,以便确保正在访问的方法确实存在于库中。

下面是一个示例:

代码语言:javascript
复制
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 的示例:

代码语言:javascript
复制
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 允许函数返回多个值。让我们演示一下,如下所示:

代码语言:javascript
复制
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 允许一个源文件导入其他源文件。下面是一个示例来演示这一点:

代码语言:javascript
复制
//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 的私有交易,因为实体之间签署的协议对它们是私有的,细节不会暴露给其他实体。尽管只有文件的哈希将被暴露,但其他实体知道一个实体签署了多少协议仍然不是一个好主意。

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

代码语言:javascript
复制
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属性时,您将获得以下输出:

代码语言:javascript
复制
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)的信息。

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

代码语言:javascript
复制
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 属性中有第二个实体的公钥。

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

代码语言:javascript
复制
contract_obj.get.call("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c
  a495991b7852b855"); 

您将获得此输出:

代码语言:javascript
复制
[1477591434, "Owner Name"] 

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

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

摘要

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 本书适合谁
  • 如何充分利用本书
  • 下载示例代码文件
  • 使用的约定
  • 第一章:什么是去中心化应用程序?
  • 什么是 DApp?
  • 什么是区块链?
  • 理解拜占庭容错
  • 用户账户的表示
  • 什么是 UTXO?
  • 流行的许可制区块链平台
  • 以太坊
  • Quorum
  • 奇偶校验
  • MultiChain
  • Hyperledger Fabric 1.0
  • BigchainDB
  • 星际文件系统
  • Corda
  • 交易的有效性
  • Hyperledger Sawtooth
  • 流行的区块链用例
  • Everledger
  • 沃尔玛的食品追踪
  • 加纳的土地登记
  • 迪拜的住房租赁
  • Ubin 项目
  • 摘要
  • 第二章:使用 Quorum 构建区块链
  • Quorum 概述
  • 以太坊账户
  • 以太坊交易是什么?
  • 什么是 Merkle 树?
  • 区块链中的分叉是什么?
  • Raft 共识
  • 伊斯坦布尔拜占庭容错
  • 私有合约和星座
  • 安装 Quorum 和星座
  • 构建你的第一个 Raft 网络
  • 设置星座网络
  • 生成 enode
  • 创建一个账户
  • 创建创世块
  • 启动节点
  • 动态添加或移除节点
  • 搭建你的第一个 IBFT 网络
  • 安装 IBFT 工具
  • 创建一个创世块
  • 起始节点
  • 动态添加或移除验证者
  • 概要
  • 第三章:编写智能合约
  • Solidity 源文件
  • 智能合约的结构
  • Solidity 中的数据位置
  • 不同类型的数据
  • 数组
  • 字符串
  • 结构体
  • 枚举
  • 映射
  • delete 运算符
  • 基本类型之间的转换
  • 使用 var
  • 控制结构
  • 使用 new 操作符创建合同
  • 异常
  • 外部函数调用
  • 合同的特性
  • 可见性
  • 函数修饰符
  • 回退函数
  • 继承
  • 超级关键字
  • 抽象合约
  • 使用 for
  • 返回多个值
  • 导入其他 Solidity 源文件
  • 全局可用变量
  • 区块和交易属性
  • 地址类型相关的变量
  • 合同相关的变量
  • 以太单位
  • 存在性、完整性和所有权证明合同
  • 编译和部署合约
  • 摘要
相关产品与服务
区块链
云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档