区块链项目构建指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

区块链是一个去中心化的账本,它维护着一个持续增长的数据记录列表,受到篡改和修订的保护。每个用户都可以连接到网络,向其发送新交易,验证交易,并创建新的区块。

本书将教会您什么是区块链,它如何维护数据完整性,以及如何使用以太坊创建真实世界的区块链项目。通过有趣的真实世界项目,您将学会如何编写智能合约,这些合约完全按照程序编写,没有欺诈、审查或第三方干预的机会,并构建端到端的区块链应用程序。您将学习加密货币中的密码学概念、以太安全、挖矿、智能合约和 Solidity 等概念。

区块链是比特币的主要技术创新,它作为比特币交易的公共账本。

本书涵盖的内容

第一章,理解去中心化应用,将解释什么是 DApps,并概述它们的工作原理。

第二章,理解以太坊的工作原理,解释了以太坊的工作原理。

第三章,编写智能合约,展示了如何编写智能合约,以及如何使用 geth 的交互式控制台使用 web3.js 部署和广播交易。

第四章,使用 web3.js 入门,介绍了 web3js 及如何导入、连接 geth,并解释如何在 Node.js 或客户端 JavaScript 中使用它。

第五章,构建钱包服务,解释了如何构建一个钱包服务,用户可以轻松创建和管理以太坊钱包,甚至是离线的。我们将专门使用 LightWallet 库来实现这一点。

第六章,构建智能合约部署平台,展示了如何使用 web3.js 编译智能合约,并使用 web3.js 和 EthereumJS 部署它。

第七章,构建一个投注应用,解释了如何使用 Oraclize 从以太坊智能合约发出 HTTP 请求,以访问来自万维网的数据。我们还将学习如何访问存储在 IPFS 中的文件,使用字符串库处理字符串等等。

第八章,构建企业级智能合约,解释了如何使用 Truffle 来轻松构建企业级 DApps。我们将通过构建一种替代货币来学习 Truffle。

第九章,构建联盟链,我们将讨论联盟链。

本书所需内容

您需要 Windows 7 SP1+、8、10 或 Mac OS X 10.8+。

本书适合对象

本书适用于现在想要使用区块链和以太坊创建防篡改数据(和交易)应用程序的 JavaScript 开发人员。对加密货币、支撑其逻辑和数据库的人将发现本书非常有用。

约定

在本书中,您将找到许多文本样式,用以区分不同类型的信息。以下是一些这些样式的示例以及它们的含义解释。

文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄显示如下:“然后,在 Final 目录中使用 node app.js 命令运行应用。”

代码块设置如下:

var solc = require("solc"); 
var input = "contract x { function g() {} }"; 
var output = solc.compile(input, 1); // 1 activates the optimizer  
for (var contractName in output.contracts) { 
    // logging code and ABI  
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 
    console.log(contractName + "; " + JSON.parse(output.contracts[contractName].interface)); 
}

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

    npm install -g solc

新术语重要词汇 以粗体显示。例如,屏幕上看到的词汇,例如菜单或对话框中的词汇,显示在文本中,如下所示:“现在再次选择同一文件,然后单击“获取信息”按钮。”

警告或重要提示会以此框的形式出现。

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

读者反馈

我们读者的反馈意见一直受欢迎。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它帮助我们开发出您真正能够充分利用的标题。

要向我们发送一般反馈意见,只需简单发送电子邮件至 feedback@packtpub.com,并在消息主题中提及书名。

如果您对某个主题具有专业知识,并且有兴趣编写或为书籍做出贡献,请查看我们的作者指南,网址为 www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的自豪所有者,我们有很多事情可以帮助您充分利用您的购买。

下载示例代码

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

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

  1. 使用您的电子邮件地址和密码登录或注册到我们的网站。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

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

  4. 在搜索框中输入书名。

  5. 选择要下载代码文件的书籍。

  6. 从下拉菜单中选择您从何处购买了本书。

  7. 单击“代码下载”。

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

  • Windows 的 WinRAR / 7-Zip

  • Mac 的 Zipeg / iZip / UnRarX

  • Linux 的 7-Zip / PeaZip

本书的代码包也托管在 GitHub 上,链接为 github.com/PacktPublishing/Building-Blockchain-Projects。我们还提供了来自我们丰富书籍和视频目录的其他代码包,可以在 github.com/PacktPublishing/ 查看!请查看!

下载本书的彩色图像

我们还为您提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从 www.packtpub.com/sites/default/files/downloads/BuildingBlockchainProjects_ColorImages.pdf 下载此文件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的书籍中发现错误——可能是文字或代码方面的错误——我们将不胜感激地向您报告。通过这样做,您可以避免其他读者的挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接,然后输入勘误的详细信息。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书籍的勘误部分的任何现有勘误列表中。

要查看先前提交的勘误,请访问 www.packtpub.com/books/content/support,然后在搜索框中输入书名。所需信息将显示在勘误部分下。

盗版

互联网上侵犯版权的行为一直是所有媒体的持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过链接至可疑盗版材料的方式联系我们,邮箱地址为copyright@packtpub.com

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,可以通过邮件联系我们,邮箱地址为questions@packtpub.com,我们将尽力解决问题。

第一章:理解去中心化应用

我们一直在使用的几乎所有基于互联网的应用都是集中式的,即每个应用的服务器都由特定公司或个人拥有。开发者一直在构建集中式应用,用户也一直在使用它们很长时间。但是集中式应用存在一些问题,使得几乎不可能构建某些类型的应用程序,并且每个应用程序最终都会有一些共同的问题。集中式应用的一些问题是较不透明,具有单一故障点,未能防止网络审查等等。由于这些问题,出现了一种新技术用于构建基于互联网的应用程序,称为去中心化应用DApps)。在本章中,我们将了解去中心化应用。

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

  • 什么是 DApp?

  • 去中心化、集中化和分布式应用之间有什么区别?

  • 集中式和去中心化应用的优缺点。

  • 一些最流行的 DApp 使用的数据结构、算法和协议概述

  • 了解一些建立在其他 DApps 之上的热门 DApps。

什么是 DApp?

DApp 是一种后端运行在去中心化对等网络上的互联网应用,其源代码是开源的。网络中的没有一个单一节点对 DApp 拥有完全控制权。

根据 DApp 的功能不同,使用不同的数据结构来存储应用数据。例如,比特币 DApp 使用区块链数据结构。

这些对等体可以是任何连接到互联网的计算机;因此,检测和防止对等体对应用数据进行无效更改并与他人共享错误信息变成一项重大挑战。因此,我们需要对等体之间就发布的数据是正确还是错误达成共识。在 DApp 中没有中央服务器来协调对等体并决定什么是对什么是错;因此,解决这个挑战变得非常困难。有一些协议(专门称为共识协议)来解决这个挑战。共识协议是专门为 DApp 使用的数据结构设计的。例如,比特币使用工作量证明协议来达成共识。

每个 DApp 都需要一个客户端供用户使用。为了使用 DApp,我们首先需要通过运行自己的节点服务器来连接客户端到节点服务器。DApp 的节点仅提供一个 API 并让开发者社区使用 API 开发各种客户端。一些 DApp 开发者正式提供客户端。DApps 的客户端应该是开源的,应该可以下载使用;否则,去中心化的整个概念将失败。

但是,客户端体系结构设置是繁琐的,特别是对于非开发人员的用户;因此,客户端通常是托管的,和/或节点作为服务托管,以使使用 DApp 的过程更加简单。

什么是分布式应用程序?

分布式应用程序是那些分布在多个服务器上而不仅仅是一个的应用程序。当应用程序数据和流量变得巨大且应用程序的停机时间是不可承受的时,就必须采用分布式应用程序。在分布式应用程序中,数据在各个服务器之间复制,以实现数据的高可用性。中心化应用程序可能是分布式的,也可能不是,但去中心化应用程序总是分布式的。例如,谷歌、Facebook、Slack、Dropbox 等都是分布式的,而简单的投资组合网站或个人博客通常在流量非常高时才会被分布。

去中心化应用程序的优势

以下是去中心化应用程序的一些优势:

  • DApps 具有容错性,因为它们默认情况下是分布式的,没有单点故障。

  • 它们可以防止网络审查的侵犯,因为没有中央机构可以被政府施压去除一些内容。政府甚至无法屏蔽应用程序的域名或 IP 地址,因为 DApps 不是通过特定的 IP 地址或域名访问的。显然,政府可以通过 IP 地址追踪网络中的个别节点并关闭它们,但如果网络很庞大,那么关闭应用程序将变得几乎不可能,特别是如果节点分布在不同国家。

  • 用户很容易信任该应用程序,因为它不受可能出于利润目的欺骗用户的单一权威机构的控制。

去中心化应用程序的缺点

显然,每个系统都有一些优点和缺点。以下是去中心化应用程序的一些缺点:

  • 修复错误或更新 DApps 非常困难,因为网络中的每个对等体都必须更新其节点软件。

  • 有些应用程序需要验证用户身份(即 KYC),由于没有中央机构来验证用户身份,因此在开发此类应用程序时会成为一个问题。

  • 它们难以构建,因为它们使用非常复杂的协议来实现共识,并且必须从一开始就构建以适应规模。因此,我们不能只是实现一个想法,然后稍后添加更多功能并扩展它。

  • 应用程序通常独立于第三方 API 来获取或存储内容。DApps 不应依赖于中心化的应用程序 API,但 DApps 可能依赖于其他 DApps。由于目前还没有一个庞大的 DApps 生态系统,因此构建 DApp 非常困难。尽管理论上 DApps 可能依赖于其他 DApps,但在实际上紧密耦合 DApps 却非常困难。

去中心化自治组织

通常,签署的文件代表组织,并且政府对其有影响力。根据组织的类型,该组织可能有或没有股东。

分散自治组织DAO)是由计算机程序代表的组织(也就是说,组织按照程序中编写的规则运行),完全透明,由股东完全控制,且不受政府影响。

要实现这些目标,我们需要开发一个 DAO 作为 DApp。因此,我们可以说 DAO 是 DApp 的子类。

Dash 和 DAC 是 DAO 的几个示例。

什么是分散自治公司(DAC)? 目前仍然没有明确区分 DAC 和 DAO 的差异。许多人认为它们是相同的,而有些人将 DAC 定义为 DAO,当 DAO 旨在为股东赚取利润时。

DApps 中的用户身份

DApps 的一个主要优点是它通常保证用户匿名性。但许多应用程序需要验证用户身份才能使用该应用程序。由于 DApp 中没有中央权威,验证用户身份成为一项挑战。

在集中式应用程序中,人们通过请求用户提交某些扫描文档、OTP 验证等方式来验证用户身份。这个过程称为了解您的客户KYC)。但是在 DApps 中没有人来验证用户身份,DApp 必须自行验证用户身份。显然,DApps 无法理解和验证扫描文档,也无法发送短信;因此,我们需要为它们提供可以理解和验证的数字身份。主要问题在于几乎没有 DApps 有数字身份,只有少数人知道如何获得数字身份。

数字身份有各种形式。目前,最推荐和流行的形式是数字证书。数字证书(也称为公钥证书或身份证书)是用于证明公钥所有权的电子文档。基本上,用户拥有私钥、公钥和数字证书。私钥是秘密的,用户不应该与任何人分享。公钥可以与任何人分享。数字证书包含公钥和关于谁拥有该公钥的信息。显然,生成此类证书并不困难;因此,数字证书始终由您可以信任的授权实体颁发。数字证书具有一个由证书颁发机构的私钥加密的字段。要验证证书的真实性,我们只需使用证书颁发机构的公钥解密该字段,如果解密成功,则我们知道该证书是有效的。

即使用户成功获取了数字身份并且它们由 DApp 验证,仍然存在一个主要问题;那就是,存在各种数字证书颁发机构,要验证数字证书,我们需要颁发机构的公钥。要包括所有颁发机构的公钥并更新/添加新的公钥是非常困难的。由于这个问题,数字身份验证程序通常包含在客户端,以便可以轻松更新。将此验证程序移到客户端并不能完全解决这个问题,因为有很多机构颁发数字证书,并跟踪所有机构,并将它们添加到客户端是繁琐的。

为什么用户不互相验证身份?

在现实生活中进行交易时,我们通常会自行验证对方的身份,或者请权威机构来验证身份。这个想法也可以应用于 DApps。用户可以在彼此进行交易之前手动验证对方的身份。这个想法适用于特定类型的 DApps,即那些人们在其中互相交易的 DApps。例如,如果一个 DApp 是一个去中心化的社交网络,那么显然无法通过这种方式验证个人资料。但如果 DApp 是用于人们买卖东西的,那么在付款之前,买家和卖家都可以验证对方的身份。虽然在交易时这个想法看起来不错,但实际上思考一下就会发现,这变得非常困难,因为你可能不想每次交易都进行身份验证,而且不是每个人都知道如何进行身份验证。例如,如果一个 DApp 是一个打车应用程序,那么显然你不会希望每次预订车辆之前都进行身份验证。但如果你偶尔进行交易并且知道如何验证身份,那么遵循这个流程是可以的。

由于这些问题,我们当前唯一的选择是由提供客户端的公司的授权人员手动验证用户身份。例如,创建比特币帐户时不需要身份证明,但是在将比特币提取到法定货币时,交易所会要求身份证明。客户端可以忽略未经验证的用户,并不让他们使用客户端。他们可以为已由他们验证身份的用户保持客户端开放。这种解决方案也会出现一些小问题;也就是说,如果您更换客户端,您将无法找到相同的一组用户进行交互,因为不同的客户端具有不同的已验证用户集。由于这个原因,所有用户可能决定只使用一个特定的客户端,从而在客户端之间产生垄断。但这并不是一个主要问题,因为如果客户端未能正确验证用户,那么用户可以轻松地切换到另一个客户端,而不会丢失关键数据,因为它们被存储为去中心化。

在应用程序中验证用户身份的想法是为了让用户在执行某种欺诈活动后难以逃脱,防止具有欺诈/犯罪背景的用户使用应用程序,并为网络中的其他用户提供相信用户是其声称的身份的手段。无论使用什么程序来验证用户身份,用户都有办法将自己代表成其他人。无论我们使用数字身份还是扫描文档进行验证都无关紧要,因为两者都可以被盗用和重复使用。重要的是要让用户难以将自己代表成其他人,并收集足够的数据来追踪用户并证明用户进行了欺诈活动。

DApps 中的用户账户

许多应用程序需要用户账户的功能。与账户相关的数据应仅由账户所有者修改。DApps 不能像中心化应用程序一样拥有基于用户名和密码的账户功能,因为密码无法证明账户数据变更是由所有者请求的。

有很多种方法可以在 DApp 中实现用户账户。但最流行的方法是使用公私钥对来表示账户。公钥的哈希是账户的唯一标识符。要更改账户数据,用户需要使用他/她的私钥签署更改。我们需要假设用户会安全地存储他们的私钥。如果用户丢失了他们的私钥,那么他们将永远无法访问自己的账户。

访问中心化应用程序

由于单点故障,DApp 不应依赖于中心化应用程序。但在某些情况下,别无选择。例如,如果一个 DApp 想要读取足球比分,那么它将从哪里获取数据?尽管一个 DApp 可以依赖于另一个 DApp,但 FIFA 为什么会创建一个 DApp 呢?FIFA 不会仅因为其他 DApps 需要数据而创建一个 DApp。这是因为提供比分的 DApp 毫无益处,因为它最终将完全由 FIFA 控制。

在某些情况下,DApp 需要从中心化应用程序获取数据。但主要问题是 DApp 如何知道从域中获取的数据没有被中间服务/人篡改,而是实际的响应。嗯,根据 DApp 的架构,有各种解决方法。例如,在以太坊中,为了让智能合约访问中心化的 API,它们可以使用 Oraclize 服务作为中间人,因为智能合约不能直接进行 HTTP 请求。Oraclize 为智能合约从中心化服务获取的数据提供了 TLSNotary 证明。

DApps 中的内部货币

对于一个中心化的应用程序来说,要长期维持下去,应用程序的所有者需要盈利才能保持其运行。DApps 没有所有者,但是像任何其他中心化应用程序一样,DApp 的节点需要硬件和网络资源来保持其运行。因此,DApp 的节点需要得到一些有用的回报来维持 DApp 的运行。这就是内部货币发挥作用的地方。大多数 DApps 都有内置的内部货币,或者我们可以说大多数成功的 DApps 都有内置的内部货币。

共识协议决定节点获得多少货币。根据共识协议,只有某些类型的节点才能获得货币。我们还可以说,贡献于保持 DApp 安全运行的节点是获得货币的节点。只读取数据的节点不会得到任何奖励。例如,在比特币中,只有矿工成功挖掘区块才能获得比特币。

最大的问题是,由于这是一种数字货币,为什么有人会看重它?嗯,根据经济学的观点,任何有需求但供应不足的东西都会有价值。

通过使用内部货币让用户付费使用 DApp 可以解决需求问题。随着越来越多的用户使用 DApp,需求也会增加,因此内部货币的价值也会相应增加。

设定一个固定数量的可生产货币使货币变得稀缺,从而提高其价值。

货币是随着时间的推移供应而不是一次性供应的。这样做是为了让进入网络的新节点也能够保持网络的安全运行并获得货币。

DApps 中内部货币的缺点

在 DApps 中拥有内部货币的唯一缺点是,DApps 不再免费使用。这是中心化应用程序获得优势的地方之一,因为中心化应用程序可以通过广告获利,为第三方应用提供高级 API 等,可以免费为用户提供服务。

在 DApps 中,我们无法集成广告,因为没有人来检查广告标准;客户端可能不显示广告,因为对他们来说显示广告没有任何好处。

什么是有权限的 DApps?

直到现在,我们一直在了解完全开放和无需许可的 DApps;也就是说,任何人都可以参与,无需建立身份。

另一方面,有权限的 DApps 并不对所有人开放参与。有权限的 DApps 继承了所有无权限 DApps 的属性,除了你需要获得参与网络的许可。有权限的 DApps 之间的许可制度各不相同。

要加入一个有权限的 DApp,你需要获得许可,因此无权限 DApps 的共识协议可能在有权限的 DApps 中不太有效;因此,它们具有不同于无权限 DApps 的共识协议。有权限的 DApps 没有内部货币。

热门 DApps

现在我们对 DApp 是什么以及它们与中心化应用的不同有一些高层次的了解,让我们探索一些流行且有用的 DApp。在探索这些 DApp 时,我们将对它们进行足够深入的探索,以了解它们的工作原理并解决各种问题,而不是深入挖掘。

比特币

比特币是一种分散的货币。比特币是最流行的 DApp,它的成功展示了 DApp 可有多强大,并鼓励人们建立其他 DApp。

在我们深入了解比特币的工作原理和为什么人们和政府认为它是一种货币之前,我们需要了解什么是分类帐和区块链。

什么是分类帐?

分类帐基本上是交易的列表。数据库与分类帐不同。在分类帐中,我们只能追加新的交易,而在数据库中,我们可以追加、修改和删除交易。数据库可以用于实现分类帐。

什么是区块链?

区块链是用于创建分散分类帐的数据结构。区块链以串行方式由块组成。一个块包含一组交易、前一个块的哈希、时间戳(表示块的创建时间)、块奖励、块编号等。每个块都包含前一个块的哈希,从而创建了相互链接的块链。网络中的每个节点都保存着区块链的副本。

工作量证明、股权证明等是用于保持区块链安全的各种共识协议。根据共识协议的不同,区块以不同的方式创建并添加到区块链中。在工作量证明中,区块是通过称为挖掘的过程创建的,这可以保持区块链的安全。在工作量证明协议中,挖掘涉及解决复杂的难题。我们将在本书后面详细了解更多关于区块链及其共识协议的知识。

比特币网络中的区块链保存着比特币交易。新的比特币通过向成功挖掘区块的节点发放新的比特币来供应到网络中。

区块链数据结构的主要优势在于自动化审计,并使应用程序透明而安全。它可以防止欺诈和腐败。根据您实现和使用的方式,它可以用于解决许多其他问题。

比特币是否合法?

首先,比特币不是一种国际货币;相反,它是一种分散的货币。国际货币大多是合法的,因为它们是一种资产,而且它们的使用是显而易见的。

主要问题是只使用货币的 DApp 是否合法。直截了当的答案是,在许多国家都是合法的。只有极少数国家已经将其定为非法,而大多数国家尚未决定。

这是一些国家已将之定为非法而大多数尚未决定的原因:

  • 由于在 DApp 中存在身份问题,在比特币中用户帐户没有任何与之相关的身份,因此它可以用于洗钱。

  • 这些虚拟货币非常波动,因此人们失去金钱的风险更大

  • 在使用虚拟货币时,逃税真的很容易

为什么有人会使用比特币?

比特币网络仅用于发送/接收比特币,什么都不是。所以你一定会想为什么会有人对比特币有需求。

以下是一些人们使用比特币的原因:

  • 使用比特币的主要优势在于,使得在世界任何地方发送和接收支付变得简单和快捷。

  • 在线支付交易费用与比特币交易费用相比昂贵

  • 黑客可以从商家那里窃取你的支付信息,但是在比特币的情况下,窃取比特币地址是完全无用的,因为为了交易有效,必须使用其关联的私钥进行签名,用户不需要与任何人分享私钥来进行付款。

以太坊

以太坊是一个分散的平台,允许我们在其上运行 DApps。这些 DApps 是使用智能合约编写的。一个或多个智能合约可以一起形成一个 DApp。以太坊智能合约是在以太坊上运行的程序。智能合约会按照编程时的准确程序运行,没有任何的停机、审查、欺诈和第三方的干涉的可能性。

使用以太坊运行智能合约的主要优势在于,它让智能合约之间的互动变得更加容易。此外,你不需要担心集成共识协议和其他事务;相反,你只需要编写应用程序逻辑。明显地,你无法使用以太坊构建任何类型的 DApp;你只能构建那些被以太坊支持的特性的 DApp。

以太坊有一种内部货币叫做以太。要部署智能合约或执行智能合约的功能,你需要以太。

本书致力于利用以太坊构建 DApps。在本书中,你将深入学习以太坊的每一个方面。

Hyperledger 项目

Hyperledger 是一个致力于构建用于构建许可 DApps 技术的项目。Hyperledger fabric(简称 fabric)是 Hyperledger 项目的一个实现。其他的实现包括 Intel Sawtooth 和 R3 Corda。

Fabric 是一个许可的分散平台,允许我们在其上运行许可的 DApps(称为链码)。我们需要部署我们自己的 Fabric 实例,然后在其上部署我们的许可 DApps。网络中的每个节点都运行 Fabric 的一个实例。Fabric 是一个即插即用的系统,你可以很容易的插拔各种共识协议和功能。

Hyperledger 使用了区块链数据结构。基于 Hyperledger 的区块链目前可以选择不采用共识协议(即NoOps 协议),或者使用PBFTPractical Byzantine Fault Tolerance)共识协议。它有一个特殊的节点叫做证书颁发机构,它控制着谁可以加入网络以及他们可以做什么。

IPFS

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

尽管它被称为分散式文件系统,但它并不遵循文件系统的一个主要属性;也就是说,当我们在文件系统中存储某些东西时,它保证会一直存在直到被删除。但 IPFS 并非如此运作。每个节点并不保存所有文件;它存储它所需要的文件。因此,如果一个文件不太受欢迎,那么显然许多节点都不会拥有它;因此,文件从网络中消失的可能性很大。因此,许多人更喜欢将 IPFS 称为分散式对等文件共享应用程序。或者,您可以将 IPFS 视为完全分散式的 BitTorrent;也就是说,它没有跟踪器并具有一些高级功能。

它是如何工作的?

让我们看一下 IPFS 的工作概述。当我们将文件存储在 IPFS 中时,它会被分成小于 256 KB 的块,并为每个块生成哈希值。网络中的节点在哈希表中保存它们需要的 IPFS 文件及其哈希值。

IPFS 文件有四种类型:blob、list、tree 和 commit。blob 表示存储在 IPFS 中的实际文件块。list 表示完整文件,因为它保存了 blob 和其他 list 的列表。由于列表可以包含其他列表,它有助于通过网络进行数据压缩。tree 表示目录,因为它保存了 blob、list、其他 tree 和 commit 的列表。而 commit 文件表示任何其他文件版本历史中的快照。由于列表、tree 和 commit 具有指向其他 IPFS 文件的链接,它们形成了 Merkle DAG。

因此,当我们想要从网络下载文件时,我们只需要 IPFS 列表文件的哈希。或者,如果我们想要下载一个目录,那么我们只需要 IPFS 树文件的哈希。

由于每个文件都由哈希值标识,因此名称不容易记住。如果我们更新了一个文件,那么我们需要与想要下载该文件的所有人共享一个新的哈希值。为了解决这个问题,IPFS 使用了 IPNS 功能,它允许使用自我认证的名称或人性化名称指向 IPFS 文件。

Filecoin

阻止 IPFS 成为分散式文件系统的主要原因是节点仅存储它们需要的文件。Filecoin 是一个类似于 IPFS 的分散式文件系统,具有内部货币以激励节点存储文件,从而提高文件可用性,使其更像一个文件系统。

网络中的节点将赚取 Filecoins 来租用磁盘空间,而要存储/检索文件,您需要花费 Filecoins。

除了 IPFS 技术之外,Filecoin 使用了区块链数据结构和可检索性证明共识协议。

在撰写本文时,Filecoin 仍在开发中,因此许多事情仍不清楚。

Namecoin

Namecoin 是一个去中心化的键值数据库。它也有一种内部货币,叫做 Namecoins。Namecoin 使用区块链数据结构和工作量证明共识协议。

在 Namecoin 中,您可以存储键值对数据。要注册键值对,您需要花费 Namecoins。一旦注册,您需要在每 35,999 个区块中更新一次;否则,与键关联的值将过期。要更新,您也需要 Namecoins。无需更新密钥;也就是说,注册密钥后,您无需花费任何 Namecoins 来保留密钥。

Namecoin 拥有命名空间功能,允许用户组织不同类型的密钥。任何人都可以创建命名空间或使用现有的命名空间来组织密钥。

一些最受欢迎的命名空间包括 a(应用程序特定数据)、d(域名规范)、ds(安全域名)、id(身份)、is(安全身份)、p(产品)等等。

.bit 域

要访问一个网站,浏览器首先需要找到与域关联的 IP 地址。这些域名和 IP 地址的映射存储在 DNS 服务器中,由大型公司和政府控制。因此,域名容易受到审查。如果网站做出非法行为、给他们造成损失或出于其他原因,政府和公司通常会阻止域名。

因此,有必要建立一个去中心化的域名数据库。由于 Namecoin 像 DNS 服务器一样存储键值数据,因此可以使用 Namecoin 来实现去中心化 DNS,这已经得到了应用。d 和 ds 命名空间包含以 .bit 结尾的键,表示 .bit 域名。从技术上讲,命名空间对键没有任何命名约定,但是所有的 Namecoin 节点和客户端都同意这个命名约定。如果我们尝试在 dds 命名空间中存储无效的键,那么客户端将过滤掉无效的键。

支持 .bit 域的浏览器需要在 Namecoin 的 dds 命名空间中查找与 .bit 域关联的 IP 地址。

dds 命名空间的区别在于 ds 存储支持 TLS 的域,而 d 存储不支持 TLS 的域。我们已经使 DNS 实现了去中心化;同样,我们也可以使 TLS 证书的签发去中心化。

这就是在 Namecoin 中 TLS 的工作原理。用户创建自签名证书并将证书哈希存储在 Namecoin 中。当支持 .bit 域的客户端尝试访问安全的 .bit 域时,它将与服务器返回的证书哈希进行匹配,并且如果匹配,则继续与服务器进行进一步的通信。

使用 Namecoin 形成的分散的 DNS 是 Zooko 三角的第一个解决方案。Zooko 三角定义了拥有三种属性的应用程序,即分散式、身份和安全。数字身份不仅用于代表一个人,还可以代表域名、公司或其他东西。

Dash

Dash 是一种类似比特币的分散式货币。Dash 使用区块链数据结构和工作量证明共识协议。Dash 解决了比特币导致的一些主要问题。以下是与比特币相关的一些问题:

  • 交易需要几分钟才能完成,在今天的世界中,我们需要交易立即完成。这是因为比特币网络中的挖矿难度被调整得平均每 10 分钟创建一个区块。我们将在本书后面更多地了解挖矿。

  • 虽然账户没有与其关联的身份,但在交易所将比特币交易为真实货币或用比特币购买东西是可追踪的;因此,这些交易所或商家可以向政府或其他机构透露你的身份。如果你运行自己的节点来发送/接收交易,那么你的 ISP 可以看到比特币地址,并使用 IP 地址追溯所有者,因为比特币网络中的广播消息没有加密。

Dash 旨在通过使交易几乎即时结算并使真实账户的背后的真实人不再被识别来解决这些问题。它还防止你的 ISP 跟踪你。

在比特币网络中,有两种节点,即矿工和普通节点。但在 Dash 中,有三种节点,即矿工,主节点和普通节点。主节点是使 Dash 如此特别的原因。

分散式治理和预算

要托管一个主节点,你需要拥有 1,000 个 Dash 和一个静态 IP 地址。在 Dash 网络中,主节点和矿工都赚取 Dash。当一个区块被挖出时,45%的奖励给矿工,45%给主节点,剩下的 10%用于预算系统。

主节点实现了分散式治理和预算。由于分散式治理和预算系统,Dash 被称为 DAO,因为它确实是这样的。

网络中的主节点就像股东一样;他们有权决定 10%的 Dash 去向。这 10%的 Dash 通常用于资助其他项目。每个主节点有权利使用一票来批准一个项目。

项目提案的讨论发生在网络之外。但投票在网络中进行。

主节点可以为 DApps 中的用户身份验证提供可能的解决方案;也就是说,主节点可以民主地选择一个节点来验证用户身份。这个节点背后的人或企业可以手动验证用户文件。部分奖励也可以给这个节点。如果节点不能提供良好的服务,那么主节点可以投票选举另一个节点。这可以是去中心化身份问题的一个良好解决方案。

去中心化服务

主节点不仅仅是批准或拒绝提案,还构成了提供各种服务的服务层。主节点提供服务的原因是,它们提供的服务越多,网络就变得越功能丰富,从而增加了用户和交易量,这就增加了 Dash 货币的价格和区块奖励,从而帮助主节点赚取更多利润。

主节点提供诸如 PrivateSend(提供匿名的币混合服务)、InstantSend(提供几乎即时的交易服务)、DAPI(提供去中心化 API,以便用户不需要运行节点)等服务。

在任何给定时间,只有 10 个主节点被选中。选择算法使用当前区块哈希来选择主节点。然后,我们从他们那里请求服务。从大多数节点接收到的响应被认为是正确的。这就是如何实现对主节点提供的服务达成共识的方式。

服务证明共识协议用于确保主节点在线、响应正常,并且其区块链是最新的。

BigChainDB

BigChainDB 允许您部署自己的权限或无权限的去中心化数据库。它使用区块链数据结构以及各种其他特定于数据库的数据结构。在撰写本文时,BigChainDB 仍在开发中,因此许多事情尚不清楚。

它还提供许多其他功能,如丰富的权限、查询、线性扩展和对多资产以及联盟共识协议的本地支持。

OpenBazaar

OpenBazaar 是一个去中心化的电子商务平台。您可以使用 OpenBazaar 购买或出售商品。在 OpenBazaar 网络中,用户并不匿名,因为他们的 IP 地址被记录下来。一个节点可以是买家、卖家或调解员。

它使用 Kademlia 风格的分布式哈希表数据结构。卖家必须托管一个节点并保持其运行,以使商品在网络中可见。

它通过使用工作量证明共识协议来防止账户垃圾邮件。它使用 proof-of-burn、CHECKLOCKTIMEVERIFY 和安全存款共识协议来防止评分和评论垃圾邮件。

买家和卖家使用比特币进行交易。买家在购买商品时可以添加一个调解员。调解员负责解决买家和卖家之间发生的任何纠纷。任何人都可以成为网络中的调解员。调解员通过解决纠纷来赚取佣金。

Ripple

Ripple 是一个去中心化的汇款平台。它允许我们转移法定货币、数字货币和大宗商品。它使用区块链数据结构,并拥有自己的共识协议。在 Ripple 的文档中,你不会找到 blocks 和 blockchain 这个术语;他们使用 ledger 这个术语。

在 Ripple 中,货币和商品的转移通过信任链方式进行,类似于哈瓦拉网络的运作方式。在 Ripple 中,有两种类型的节点,即网关和常规节点。网关支持一个或多个货币和/或商品的存款和提款。要成为 Ripple 网络中的网关,您需要作为网关获得许可以形成信任链。网关通常是注册的金融机构、交易所、商家等。

每个用户和网关都有一个帐户地址。每个用户都需要将他们信任的网关的地址添加到信任列表中。没有共识找到信任谁;这完全取决于用户,并且用户承担信任网关的风险。甚至网关也可以添加他们信任的网关列表。

让我们来看一个示例,用户 X 住在印度,想向住在美国的用户 Y 发送 500 美元。假设印度有一个名为 XX 的网关,它接受现金(实体现金或网站上的卡支付)并仅在 Ripple 上给你印度卢比余额,X 将访问 XX 的办事处或网站,存入 30,000 印度卢比,然后 XX 将广播一笔交易,表示我欠 X 30,000 印度卢比。现在假设美国有一个名为 YY 的网关,它仅允许美元交易,并且 Y 信任 YY 网关。现在,假设网关 XX 和 YY 互不信任。由于 X 和 Y 不信任一个共同的网关,XX 和 YY 互不信任,最终 XX 和 YY 不支持相同的货币。因此,要让 X 向 Y 发送资金,他需要找到中间网关以形成信任链。假设还有另一个网关 ZZ,它被 XX 和 YY 信任,并支持美元和印度卢比。因此,现在 X 可以通过将 50,000 印度卢比从 XX 转移到 ZZ 发送交易,并由 ZZ 转换为美元,然后 ZZ 将资金发送给 YY,要求 YY 将资金交给 Y。现在,与其说 X 欠 Y 500 美元,不如说 YY 欠 Y 500 美元,ZZ 欠 YY 500 美元,XX 欠 ZZ 30,000 印度卢比。但这都没关系,因为他们彼此信任,而之前 X 和 Y 不信任对方。但是 XX、YY 和 ZZ 可以随时在 Ripple 外转移资金,否则逆向交易会扣除这个价值。

Ripple 还有一种名为 XRP(或水波)的内部货币。发送到网络的每笔交易都会消耗一些水波。由于 XRP 是水波的本地货币,因此可以向网络中的任何人发送,而无需信任。XRP 也可以在形成信任链时使用。请记住,每个网关都有自己的货币兑换率。XRP 不是通过挖矿过程生成的;相反,在开始时生成了总共 1000 亿个 XRP,并由水波公司自己拥有。根据各种因素手动提供 XRP。

所有交易都记录在去中心化账本中,形成不可变的历史。需要共识确保所有节点在某一时间点具有相同的账本。在 Ripple 中,还有一种称为验证者的第三种节点,它们是共识协议的一部分。验证者负责验证交易。任何人都可以成为验证者。但其他节点会保留可以实际信任的验证者列表。这个列表被称为 UNL(唯一节点列表)。验证者也有一个 UNL;即,它信任的验证者也希望达成共识。目前,Ripple 决定了可以信任的验证者列表,但如果网络认为 Ripple 选择的验证者不值得信任,那么他们可以在其节点软件中修改列表。

你可以通过获取前一个账本并应用自那时起发生的所有交易来形成一个账本。因此,要就当前账本达成一致,节点必须就前一个账本和自那时起发生的交易集达成一致。创建新账本后,节点(包括普通节点和验证者)启动计时器(几秒钟,大约 5 秒钟),并收集在上一个账本创建期间到达的新交易。当计时器到期时,它会选择至少 80%的 UNL 认为有效的那些交易,并形成下一个账本。验证者向网络广播一个提案(一组他们认为有效的交易,以形成下一个账本)。验证者可以多次广播对同一个账本的提案,如果他们决定根据来自其 UNL 和其他因素的提案更改有效交易的列表。因此,您只需要等待 5-10 秒,即可确保您的交易已被网络确认。

有些人想知道这是否会导致账本出现许多不同版本,因为每个节点可能有不同的 UNL。只要 UNL 之间存在最小程度的互联性,就会迅速达成共识。这主要是因为每个诚实的节点的主要目标是达成共识。

摘要

在本章中,我们学习了什么是 DApps,并简要了解了它们的工作原理。我们看到了一些 DApps 面临的挑战,以及这些问题的各种解决方案。最后,我们了解了一些流行的 DApps,并对它们的特点和工作原理有了一个概览。现在,你应该能够清楚地解释什么是 DApp,以及它是如何工作的。

第二章:了解以太坊的工作原理

在上一章中,我们看到了什么是 DApps。我们还看到了一些流行 DApps 的概述。其中之一是以太坊。目前,以太坊是继比特币之后最流行的 DApp。在本章中,我们将深入学习以太坊的工作原理以及我们可以使用以太坊开发什么。我们还将看到重要的以太坊客户端和节点实现。

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

  • 以太坊用户账户

  • 智能合约是什么,它们是如何工作的?

  • 以太坊虚拟机

  • 工作量证明共识协议中的挖掘是如何工作的?

  • 学习如何使用 geth 命令

  • 设置以太坊钱包和 Mist

  • Whisper 和 Swarm 概述

  • 以太坊的未来

以太坊概述

以太坊是一个分散式平台,允许我们在其上部署 DApps。智能合约使用 Solidity 编程语言编写。DApps 使用一个或多个智能合约创建。智能合约是完全按照程序运行的程序,没有任何停机、审查、欺诈或第三方接口的可能性。在以太坊中,智能合约可以用几种编程语言编写,包括 Solidity、LLL 和 Serpent。Solidity 是其中最流行的语言。以太坊有一种内部货币称为以太。部署智能合约或调用其方法需要以太。就像任何其他 DApp 一样,智能合约可以有多个实例,每个实例都由其唯一地址标识。用户账户和智能合约都可以持有以太。

以太坊使用区块链数据结构和工作量证明共识协议。智能合约的一种方法可以通过交易或另一种方法调用。网络中有两种类型的节点:常规节点和矿工。常规节点只是拥有区块链的副本,而矿工通过挖掘区块来构建区块链。

以太坊账户

要创建一个以太坊账户,我们只需要一个非对称密钥对。有各种算法,如 RSA、ECC 等,用于生成非对称加密密钥。以太坊使用椭圆曲线加密ECC)。ECC 有各种参数。这些参数用于调整速度和安全性。以太坊使用secp256k1参数。深入了解 ECC 及其参数将需要数学知识,并且对于使用以太坊构建 DApps 而言,深入理解它并非必需。

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

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

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

  2. 放弃前 96 位,也就是 12 个字节。你现在应该有 160 个位的二进制数据,也就是 20 个字节。

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

现在任何人都可以向这个地址发送以太币。

交易

交易是一个签名的数据包,用于将以太坊从一个账户转移到另一个账户或合约,调用合约的方法,或部署新的合约。交易使用ECDSA椭圆曲线数字签名算法)进行签名,这是基于 ECC 的数字签名算法。交易包含了消息的接收者,用于识别发送者并证明其意图的签名,要转移的以太币数量,交易执行允许的最大计算步骤数(称为 gas 限制),以及发送者愿意支付每个计算步骤的费用(称为 gas 价格)。如果交易的目的是调用合约的方法,它还包含了输入数据;或者如果其目的是部署合约,那么它可以包含初始化代码。gas 使用量和 gas 价格的乘积被称为交易费用。要发送以太币或执行合约方法,你需要向网络广播一笔交易。发送者需要用私钥对交易进行签名。

如果我们确信一笔交易将永远出现在区块链中,那么这笔交易就被确认。建议在假定一笔交易已确认之前等待 15 个确认。

共识

以太坊网络中的每个节点都保存着区块链的副本。我们需要确保节点无法篡改区块链,还需要一种机制来检查一个区块是否有效。此外,如果我们遇到两个不同的有效区块链,我们需要一种方法来找出选择哪一个。

以太坊使用工作量证明共识协议来保持区块链的防篡改性。工作量证明系统涉及解决一个复杂的谜题以创建一个新的区块。解决这个谜题应该需要大量的计算能力,因此难以创建区块。在工作量证明系统中创建区块的过程称为挖矿。矿工是网络中挖矿的节点。使用工作量证明的所有 DApp 并不完全实现相同的一组算法。它们可能在矿工需要解决的谜题,谜题的难度,解决时间等方面有所不同。我们将学习有关以太坊工作量证明的内容。

任何人都可以成为网络中的矿工。每个矿工都独立解决谜题;第一个解决谜题的矿工是赢家,并且将获得五个以太和该区块中所有交易的交易费用。如果你拥有比网络中任何其他节点更强大的处理器,这并不意味着你总会成功,因为谜题的参数对所有矿工来说并不完全相同。但是,如果你拥有比网络中任何其他节点更强大的处理器,这会增加你成功的机会。工作证明的行为类似于彩票系统,处理能力可以被视为一个人拥有的彩票数量。网络安全性不是由矿工的总数来衡量的;相反,它是由网络的总处理能力来衡量的。

区块链可以拥有的区块数量没有限制,可以生产的总以太币数量也没有限制。一旦一个矿工成功挖掘一个区块,它就会将该区块广播到网络中的所有其他节点。一个区块有一个头和一组交易。每个区块都持有前一个区块的哈希,从而创建了一个连接的链。

让我们看看矿工需要解决的难题是什么,以及在高层次上如何解决它。要挖掘一个区块,首先,矿工收集到的新未开采的交易被广播到它,然后过滤掉无效的交易。一个有效的交易必须使用私钥正确签名,账户必须有足够的余额来进行交易等等。现在矿工创建一个区块,它有一个头和内容。内容是该区块包含的交易列表。头包含诸如前一个区块的哈希、区块号、随机数、目标、时间戳、难度、矿工地址等内容。时间戳表示区块创建时的时间。然后随机数是一个毫无意义的值,它被调整以找到谜题的解决方案。这个谜题基本上是找到这样的随机数值,当区块被散列时,散列小于或等于目标。以太坊使用 ethash 哈希算法。找到随机数的唯一方法是枚举所有可能性。目标是一个 256 位数字,它是根据各种因素计算出来的。头中的难度值是目标的不同表示,以便更容易处理。目标越低,找到随机数的时间就越长,目标越高,找到随机数的时间就越短。这是计算谜题难度的公式:

current_block_difficulty = previous_block_difficulty + previous_block_difficulty // 2048 * max(1 - (current_block_timestamp - previous_blocktimestamp) // 10, -99) + int(2 ** ((current_block_number // 100000) - 2)) 

现在网络中的任何节点都可以通过首先检查区块链中的交易是否有效、时间戳验证,然后检查所有区块的目标和随机数是否有效,矿工是否为自己分配了有效的奖励等等来检查他们所拥有的区块链是否有效。

如果网络中的一个节点接收到两个不同的有效区块链,那么所有区块的综合难度更高的区块链将被视为有效的区块链。

现在,举个例子,如果网络中的一个节点改变了某个区块中的一些交易,那么该节点需要计算所有后续区块的随机数。当它重新找到后续区块的随机数时,网络可能已经挖掘了更多的区块,因此将拒绝此区块链,因为其综合难度将较低。

时间戳

计算区块目标的公式需要当前时间戳,而且每个区块的头部都附有当前时间戳。没有任何东西能阻止矿工在挖掘新区块时使用其他时间戳而不是当前时间戳,但他们通常不会这样做,因为时间戳验证会失败,其他节点不会接受该区块,而且这将是矿工资源的浪费。当一个矿工广播一个新挖掘的区块时,它的时间戳会通过检查该时间戳是否大于上一个区块的时间戳来进行验证。如果一个矿工使用的时间戳大于当前时间戳,则难度将较低,因为难度与当前时间戳成反比;因此,区块时间戳为当前时间戳的矿工将被网络接受,因为它的难度将更高。如果一个矿工使用的时间戳大于上一个区块的时间戳且小于当前时间戳,则难度将更高,因此,挖掘区块将需要更多的时间;在区块被挖掘时,网络可能已经产生了更多的区块,因此,这个区块将被拒绝,因为恶意矿工的区块链的难度将低于网络的区块链。由于这些原因,矿工们总是使用准确的时间戳,否则他们将一无所获。

随机数

随机数是一个 64 位无符号整数。随机数是谜题的解答。矿工不断递增随机数,直到找到解答。现在你一定在想,如果有一名矿工的哈希功率超过网络中的任何其他矿工,那么该矿工是否总能第一个找到随机数?嗯,并不是。

矿工正在挖掘的区块的哈希对于每个矿工都是不同的,因为哈希依赖于时间戳、矿工地址等因素,而且不太可能对所有矿工都相同。因此,这不是一个解决难题的竞赛,而是一个抽奖系统。但当然,根据其哈希功率,一个矿工可能会有好运气,但这并不意味着矿工总能找到下一个区块。

区块时间

我们之前看到的区块难度公式使用了一个 10 秒的阈值,以确保父区块和子区块的挖掘时间之差在 10-20 秒之间。但为什么是 10-20 秒而不是其他值呢?为什么存在这样一个恒定的时间差限制,而不是一个恒定的难度呢?

想象一下,我们有一个恒定的难度,矿工只需要找到一个随机数(nonce),使得区块的哈希值小于或等于难度。假设难度很高;在这种情况下,用户将无法知道发送以太币给另一个用户需要多长时间。如果网络的计算能力不足以快速找到满足难度的随机数,则可能需要很长时间。有时,网络可能会很幸运地快速找到随机数。但这种系统很难吸引用户,因为用户总是想知道交易完成需要多长时间,就像我们从一个银行账户向另一个银行账户转账时,会给出一个应该在其中完成的时间段。如果恒定的难度较低,它将危害区块链的安全性,因为大型矿工可以比小型矿工更快地挖掘区块,而网络中最大的矿工将有能力控制 DApp。不可能找到一个能使网络稳定的恒定难度值,因为网络的计算能力不是恒定的。

现在我们知道为什么我们应该始终有一个网络挖掘一个区块需要多长时间的平均时间了。现在的问题是,最适合的平均时间是多长,因为它可以是从 1 秒到无限秒的任何值。通过降低难度可以实现较小的平均时间,通过增加难度可以实现较高的平均时间。但较低和较高平均时间的优缺点是什么?在讨论这个问题之前,我们需要先知道什么是陈旧区块。

如果两个矿工几乎同时挖掘出下一个区块会发生什么?这两个区块肯定都是有效的,但区块链不能容纳两个具有相同区块编号的区块,而且两个矿工也不能都获得奖励。尽管这是一个常见的问题,但解决方法很简单。最终,难度较高的区块将被网络接受。因此,最终被留下的有效区块被称为陈旧区块。

网络中产生的陈旧区块的总数量与生成新区块的平均时间成反比。更短的区块生成时间意味着新挖出的区块在整个网络中传播的时间更短,多于一个矿工找到谜题解决方案的机会更大,因此在区块通过网络传播时,其他矿工也可能已经解决了谜题并进行了广播,从而产生了陈旧区块。但是,如果平均区块生成时间更长,则多个矿工有较小机会解决谜题,即使他们解决了谜题,解决之间可能存在时间差,在此期间第一个解决的区块可以传播,其他矿工可以停止挖掘该区块,并转向挖掘下一个区块。如果网络中频繁出现陈旧区块,则会造成重大问题,但是如果很少出现陈旧区块,则不会造成危害。

那么陈旧区块有什么问题?它们延迟了交易的确认。当两个矿工几乎同时挖掘一个区块时,它们可能没有相同的交易集,因此如果我们的交易出现在其中一个区块中,我们不能说它已被确认,因为包含该交易的区块可能是陈旧的。我们需要等待更多的区块被挖掘。由于陈旧区块的存在,平均确认时间不等于平均区块生成时间。

陈旧区块会影响区块链安全吗?是的,会。我们知道,网络安全是由网络中矿工的总计算能力来衡量的。当计算能力增加时,难度也会增加,以确保区块不会比平均区块时间提前产生。因此,更高的难度意味着更安全的区块链,因为要篡改节点,现在需要更多的哈希算力,这使得篡改区块链更加困难;因此,可以说区块链更安全。当几乎同时挖掘两个区块时,我们将把网络分成两部分,分别为两个不同的区块链,但其中一个将成为最终的区块链。因此,工作在陈旧区块上的网络部分在陈旧区块上挖掘下一个区块,这导致网络的哈希算力损失,因为哈希算力被用于一些不必要的事情。网络的两个部分挖掘下一个区块的时间可能比平均区块时间长,因为它们丢失了哈希算力;因此,在挖掘下一个区块后,难度将减少,因为挖掘该区块所需的时间比平均区块时间长。难度的降低影响整体区块链安全性。如果陈旧率过高,它将对区块链安全性造成巨大影响。

以太坊利用所谓的幽灵协议来解决陈旧区块带来的安全问题。以太坊使用了实际幽灵协议的修改版本。幽灵协议通过简单地将陈旧区块添加到主区块链中来掩盖安全问题,从而增加了区块链的总难度,因为区块链的总难度也包括陈旧区块的难度之和。但是如何将陈旧区块插入主区块链而不发生交易冲突呢?嗯,任何区块都可以指定 0 个或多个陈旧区块。为了激励矿工将陈旧区块包含在内,矿工会因包含陈旧区块而获得奖励。而且,陈旧区块的挖矿者也会获得奖励。陈旧区块中的交易不用于计算确认,并且,陈旧区块的挖矿者不会收到陈旧区块中包含的交易的交易费。请注意,以太坊将陈旧区块称为叔区块。

这里是计算陈旧区块挖矿者获得多少奖励的公式。剩余的奖励归侄子区块,即包含孤立区块的区块:

(uncle_block_number + 8 - block_number) * 5 / 8 

由于不奖励陈旧区块的挖矿者不会损害任何安全性,你可能会想为什么陈旧区块的挖矿者会得到奖励?嗯,当网络中频繁出现陈旧区块时会引起另一个问题,这个问题通过奖励陈旧区块的挖矿者来解决。挖矿者应该获得与其为网络贡献的哈希算力百分比相似的奖励百分比。当两个不同的挖矿者几乎同时挖掘出一个区块时,由于挖矿者挖掘下一个区块的效率更高,更有可能将由哈希算力更大的挖矿者挖掘的区块添加到最终的区块链中;因此,小挖矿者将失去奖励。如果陈旧率低,这不是一个大问题,因为大挖矿者将获得少量奖励增加;但是如果陈旧率高,就会引起一个大问题,即网络中的大挖矿者最终将获得比应该获得的更多的奖励。幽灵协议通过奖励陈旧区块的挖矿者来平衡这一点。由于大挖矿者并不获取所有奖励,但获取比应该得到的更多,因此我们不像侄子区块一样奖励陈旧区块的挖矿者;而是奖励更少的金额来平衡。前述公式相当好地平衡了这一点。

幽灵限制了侄子可以引用的陈旧区块的总数,以防止矿工简单地挖掘陈旧区块并使区块链停滞。

不管在网络中出现多少陈旧的区块,都会在某种程度上影响网络。陈旧区块的频率越高,网络受到的影响就越大。

分叉

当节点之间就区块链的有效性存在冲突时,即网络中存在多个区块链,并且每个区块链都被某些矿工验证时,就会发生分叉。有三种类型的分叉:常规分叉、软分叉和硬分叉。

常规分叉是由于两个或多个矿工几乎同时找到一个区块而发生的暂时冲突。当其中一个的难度高于另一个时,冲突将得到解决。

对源代码的更改可能导致冲突。根据冲突的类型,可能需要拥有超过 50%哈希算力的矿工进行升级,或者所有矿工进行升级以解决冲突。当需要拥有超过 50%哈希算力的矿工进行升级以解决冲突时,称为软分叉;而当需要所有矿工进行升级以解决冲突时,则称为硬分叉。软分叉的一个例子是,如果对源代码的更新使一部分旧区块/交易无效,那么当超过 50%的哈希算力的矿工进行了升级后,这个问题可以解决,因为新的区块链将具有更高的难度最终被整个网络接受。硬分叉的一个例子是,如果源代码的更新是为了更改矿工的奖励,那么所有矿工都需要进行升级以解决冲突。

自发布以来,以太坊经历了各种硬分叉和软分叉。

创世块

创世块是区块链中的第一个块。它被分配到块编号 0。它是区块链中唯一一个不引用以前块的块,因为以前没有任何块。它不包含任何交易,因为目前还没有产生任何以太币。

两个网络中的节点只有在它们都拥有相同的创世块(genesis block)时才会配对,也就是说,只有当两个节点拥有相同的创世块时,区块同步才会发生,否则它们将互相拒绝。高难度的不同创世块不能取代低难度的创世块。每个节点都生成自己的创世块。对于各种网络,创世块是硬编码到客户端中的。

以太币面额

以太币和任何其他货币一样,有各种面额。以下是各种面额:

  • 1 以太币 = 1000000000000000000 维(Wei)

  • 1 以太币 = 1000000000000000 千维(Kwei)

  • 1 以太币 = 1000000000000 英美制微(Mwei)

  • 1 以太币 = 1000000000 吉(Gwei)

  • 1 以太币 = 1000000 萨博(Szabo)

  • 1 以太币 = 1000 芬尼(Finney)

  • 1 以太币 = 0.001 开斯(Kether)

  • 1 以太币 = 0.000001 兆斯(Mether)

  • 1 以太币 = 0.000000001 盖撒币(Gether)

  • 1 以太币 = 0.000000000001 泰达币(Tether)

以太坊虚拟机

EVM(或以太坊虚拟机)是以太坊智能合约字节码执行环境。网络中的每个节点都运行 EVM。所有节点都使用 EVM 执行指向智能合约的交易,因此每个节点都进行相同的计算并存储相同的值。只转移以太币的交易也需要一些计算,即找出地址是否有余额,并相应地扣除余额。

每个节点都执行交易并存储最终状态,原因有很多。例如,如果有一个存储参加派对的每个人的姓名和详情的智能合约,每当添加一个新人时,一个新的交易就会被广播到网络中。对于网络中的任何节点来说,他们只需要读取合约的最终状态就可以显示参加派对的每个人的详情。

网络中的每笔交易都需要进行一些计算和存储。因此,需要有一定的交易费用,否则整个网络将被垃圾邮件交易淹没。此外,如果没有交易成本,矿工将没有理由将交易包含在区块中,他们将开始挖掘空块。每笔交易需要不同量的计算和存储;因此,每笔交易都有不同的交易成本。

EVM 有两种实现,即字节码虚拟机和 JIT-VM。在编写本书时,JIT-VM 可以使用,但其开发尚未完成。无论哪种情况,Solidity 代码都会被编译成字节码。在 JIT-VM 的情况下,字节码会进一步被编译。JIT-VM 比其对应的更高效。

气体

气体是计算步骤的度量单位。每笔交易都需要包括一个气体限制和它愿意支付的每单位气体费用(即每次计算的费用);矿工可以选择包含该交易并收取该费用。如果交易使用的气体少于或等于气体限制,交易将被处理。如果总气体超过了气体限制,那么所有的更改都将被撤销,除了交易仍然有效,矿工仍然可以收取费用(即最大可使用的气体和气体价格的乘积)。

矿工决定气体价格(即每次计算的价格)。如果一笔交易的气体价格低于矿工决定的气体价格,矿工将拒绝挖掘该交易。气体价格是以 wei 为单位的一笔金额。因此,如果气体价格低于矿工所需的价格,矿工可以拒绝在区块中包含交易。

EVM 中的每个操作都被分配了消耗的气体数量。

交易成本会影响账户可以向另一个账户转移的最大以太币数量。例如,如果一个账户有五个以太币的余额,它不能将所有五个以太币转移到另一个账户,因为如果所有以太币都转移了,那么账户中就没有余额可以从中扣除交易费用。

如果一个交易调用了一个合约方法,并且该方法发送了一些以太币或调用了其他合约方法,交易费将从调用合约方法的账户中扣除。

对等发现

要使节点成为网络的一部分,它需要连接到网络中的一些其他节点,以便它可以广播交易/区块并监听新的交易/区块。一个节点不需要连接到网络中的每个节点;相反,一个节点连接到一些其他节点。而这些节点连接到另一些节点。通过这种方式,整个网络相互连接。

但是节点如何在网络中找到其他节点呢?因为没有一个所有人都可以连接的中央服务器来交换信息。以太坊有自己的节点发现协议来解决这个问题,该协议基于 Kadelima 协议。在节点发现协议中,我们有一种特殊类型的节点称为引导节点。引导节点在一段时间内维护着与它们连接的所有节点的列表。它们不保存区块链本身。当节点连接到以太坊网络时,它们首先连接到引导节点,后者共享了最后一个预定义时间段内连接到它们的节点的列表。连接的节点然后连接并与节点同步。

可以有各种各样的以太坊实例,即各种网络,每个网络都有自己的网络 ID。两个主要的以太坊网络是主网和测试网。主网是在交易所交易其以太币的网络,而测试网是开发者用来测试的。到目前为止,我们已经了解了关于主网区块链的所有内容。

Bootnode 是以太坊引导节点的最流行实现。如果你想托管自己的引导节点,可以使用 bootnode。

Whisper 和 Swarm

Whisper 和 Swarm 分别是由以太坊开发者开发的去中心化通信协议和去中心化存储平台。Whisper 是一个去中心化的通信协议,而 Swarm 是一个去中心化的文件系统。

Whisper 让网络中的节点相互通信。它支持广播、用户间加密消息等。它不是设计用来传输大量数据的。你可以在github.com/ethereum/wiki/wiki/Whisper了解更多关于 Whisper 的信息,也可以在github.com/ethereum/wiki/wiki/Whisper-Overview查看代码示例概述。

Swarm 类似于 Filecoin,主要区别在于技术和激励机制。 Filecoin 不惩罚存储,而 Swarm 惩罚存储,因此进一步增加了文件的可用性。 你可能会想了解 Swarm 中的激励机制是如何工作的。 它是否有内部货币? 实际上,Swarm 没有内部货币,而是使用以太坊的激励机制。 以太坊中有一个智能合约,用于跟踪激励机制。显然,智能合约无法与 Swarm 通信;相反,Swarm 与智能合约通信。 因此,你通过智能合约支付存储,付款在到期日期之后释放给存储。 你还可以向智能合约举报文件丢失,这种情况下它可以惩罚相应的存储。 你可以在github.com/ethersphere/go-ethereum/wiki/IPFS-&-SWARM中了解更多有关 Swarm 和 IPFS/Filecoin 之间的区别,并在github.com/ethersphere/go-ethereum/blob/bzz-config/bzz/bzzcontract/swarm.sol上查看智能合约代码。

在撰写本书时,Whisper 和 Swarm 仍在开发中,因此许多事情仍不清楚。

Geth

Geth(或称为 go-ethereum)是以太坊、Whisper 和 Swarm 节点的实现。 Geth 可用于成为所有这些的一部分或仅选定的一部分。 将它们合并的原因是使它们看起来像一个单一的 DApp,以便通过一个节点,客户端可以访问所有三个 DApps。

Geth 是一个命令行应用程序。 它是用 go 编程语言编写的。 它适用于所有主要操作系统。 目前的 geth 版本尚未支持 Swarm,并且仅支持 Whisper 的一些功能。在撰写本书时,最新版本的 geth 是 1.3.5。

安装 geth

Geth 适用于 OS X、Linux 和 Windows。 它支持两种安装类型:二进制安装和脚本安装。 在撰写本书时,最新的稳定版本是 1.4.13. 让我们看看如何在各种操作系统上使用二进制安装方法安装它。 当你必须修改 geth 源代码并安装它时,才使用脚本化安装。 我们不希望对源代码进行任何更改,因此我们将选择二进制安装。

OS X

在 OS X 上安装 geth 的推荐方法是使用 brew。 在终端中运行这两个命令以安装 geth:

brew tap ethereum/ethereum 
brew install ethereum  

Ubuntu

推荐在 Ubuntu 上安装 geth 的方法是使用apt-get。 在 Ubuntu 终端中运行这些命令以安装 geth:

sudo apt-get install software-properties-common 
sudo add-apt-repository -y ppa:ethereum/ethereum 
sudo apt-get update 
sudo apt-get install ethereum

Windows

Geth 为 Windows 提供可执行文件。从 github.com/ethereum/go-ethereum/wiki/Installation-instructions-for-Windows 下载 zip 文件,并进行解压。在其中,您将找到 geth.exe 文件。

要了解更多关于在各种操作系统上安装 geth 的信息,请访问github.com/ethereum/go-ethereum/wiki/Building-Ethereum

JSON-RPC 和 JavaScript 控制台

Geth 为其他应用程序提供了 JSON-RPC API 以进行通信。Geth 使用 HTTP、WebSocket 和其他协议提供 JSON-RPC API。JSON-RPC 提供的 API 分为以下类别:admin、debug、eth、miner、net、personal、shh、txpool 和 web3。您可以在此处找到更多关于它的信息github.com/ethereum/go-ethereum/wiki/JavaScript-Console

Geth 还提供了一个交互式 JavaScript 控制台,以便使用 JavaScript API 与其进行程序化交互。此交互式控制台使用 JSON-RPC 通过 IPC 与 geth 进行通信。我们将在后续章节中了解更多关于 JSON-RPC 和 JavaScript API 的内容。

子命令和选项

让我们通过示例来学习 geth 命令的一些重要子命令和选项。您可以使用 help 子命令找到所有子命令和选项的列表。在接下来的章节中,我们将看到更多关于 geth 及其命令的内容。

连接到主网网络

以太坊网络中的节点默认使用 30303 端口进行通信。但节点也可以选择监听其他端口号。

要连接到主网网络,只需运行 geth 命令。以下是明确指定网络 ID 和指定 geth 将存储下载的区块链的自定义目录的示例:

    geth --datadir "/users/packt/ethereum" --networkid 1

--datadir 选项用于指定区块链存储位置。如果没有提供,默认路径为 $HOME/.ethereum

--networkid 用于指定网络 ID。1 是主网的 ID。如果未提供,默认值为 1。测试网的网络 ID 为 2。

创建私有网络

要创建一个私有网络,只需提供一个随机网络 ID。私有网络通常用于开发目的。Geth 还提供了与日志记录和调试相关的各种标志,在开发过程中非常有用。因此,我们可以简单地使用 --dev 标志,该标志会启用各种调试和日志记录标志来运行私有网络,而无需提供随机网络 ID 和各种日志记录和调试标志。

创建账户

Geth 还允许我们创建账号,即生成与其关联的密钥和地址。要创建账户,请使用以下命令:

    geth account new

运行此命令时,将要求您输入密码来加密您的账户。如果忘记密码,将无法访问您的账户。

要获取本地钱包中所有账户的列表,请使用以下命令:

    geth account list

上述命令将打印所有账户的地址列表。密钥默认存储在--datadir路径中,但您可以使用--keystore选项指定其他目录。

挖矿

默认情况下,geth 不会开始挖矿。要指示 geth 开始挖矿,只需提供 --mine 选项。还有一些与挖矿相关的其他选项:

    geth --mine --minerthreads 16 --minergpus '0,1,2' --etherbase '489b4e22aab35053ecd393b9f9c35f4f1de7b194' --unlock '489b4e22aab35053ecd393b9f9c35f4f1de7b194'

在此,除了--mine选项外,我们还提供了各种其他选项。--minerthreads选项指定哈希时要使用的总线程数。默认情况下,使用八个线程。Etherbase 是挖矿获得的奖励存入的地址。默认情况下,账户是加密的。因此,为了访问账户中的以太币,我们需要解锁它,即解密账户。解密用于解密与账户关联的私钥。要开始挖矿,我们不需要解锁它,因为只需要地址来存入挖矿奖励。可以使用 -unlock 选项解锁一个或多个账户。通过逗号分隔地址可以提供多个地址。

--minergpus 用于指定用于挖矿的 GPU。要获取 GPU 列表,请使用 geth gpuinfo 命令。每个 GPU 需要 1-2 GB 的 RAM。默认情况下,它不使用 GPU,而只使用 CPU。

快速同步

撰写本书时,区块链的大小约为 30GB。如果您的互联网连接速度慢,下载可能需要几个小时或几天。以太坊实现了一个快速同步算法,可以更快地下载区块链。

快速同步不会下载整个区块;相反,它只下载区块头、交易收据和最近的状态数据库。因此,我们不必下载和重放所有交易。为了检查区块链的完整性,该算法在每个定义的区块数量之后下载一个完整的区块。要了解有关快速同步算法的更多信息,请访问github.com/ethereum/go-ethereum/pull/1889

在下载区块链时使用快速同步,您需要在运行 geth 时使用--fast标志。

由于安全原因,快速同步只会在初始同步期间运行(即当节点自己的区块链为空时)。当节点成功与网络同步后,快速同步将永久禁用。作为额外的安全功能,如果快速同步在随机轴点附近或之后失败,它会被禁用作为安全预防措施,节点将恢复到完全基于区块处理的同步。

以太坊钱包

以太坊钱包是一个以太坊 UI 客户端,允许您创建账户、发送以太币、部署合约、调用合约的方法等等。

以太坊钱包随附了 geth。当您运行以太坊时,它会尝试找到本地的 geth 实例并连接到它,如果找不到正在运行的 geth,则启动自己的 geth 节点。以太坊钱包使用 IPC 与 geth 进行通信。Geth 支持基于文件的 IPC。

如果在运行 geth 时更改数据目录,则还会更改 IPC 文件路径。因此,为了让以太坊钱包找到并连接到您的 geth 实例,您需要使用 --ipcpath 选项将 IPC 文件位置指定为其默认位置,以便以太坊钱包可以找到它;否则,以太坊钱包将无法找到它,并将启动自己的 geth 实例。要找到默认的 IPC 文件路径,请运行 geth 帮助,并且它将在 --ipcpath 选项旁边显示默认路径。

访问 github.com/ethereum/mist/releases 下载以太坊钱包。它适用于 Linux、OS X 和 Windows。与 geth 一样,它有两种安装模式:二进制和脚本安装。

这是一个显示以太坊钱包外观的图像:

Mist

Mist 是以太坊、Whisper 和 Swarm 的客户端。它让我们发送交易、发送 Whisper 消息、检查区块链等等。

Mist 与 geth 的关系类似于以太坊钱包与 geth 的关系。

Mist 最受欢迎的功能是它带有一个浏览器。目前,在浏览器中运行的前端 JavaScript 可以使用 web3.js 库(一种提供以太坊控制台 JavaScript API 以便其他应用程序与 geth 通信的库)访问 geth 节点的 web3 API。

Mist 的基本理念是构建第三代互联网(Web 3.0),通过使用以太坊、Whisper 和 Swarm 替代集中式服务器,从而消除了需要服务器的需求。

这是一张图像,展示了 Mist 的外观:

弱点

每个系统都有一些弱点。同样,以太坊也有一些弱点。显然,就像任何其他应用程序一样,以太坊源代码可能存在错误。而且就像任何其他基于网络的应用程序一样,以太坊也容易受到 DoS 攻击。但让我们看看以太坊的独特和最重要的弱点。

Sybil 攻击

攻击者可以试图填充网络,控制由他控制的普通节点;然后你很可能只连接到攻击者节点。一旦你连接到攻击者节点,攻击者就可以拒绝中继所有人的区块和交易,从而使你与网络断开连接。攻击者只能中继他创建的区块,从而将您置于另一个网络中,依此类推。

51% 攻击

如果攻击者控制了超过一半的网络算力,那么攻击者可以比网络中其他部分更快地生成区块。攻击者可以简单地保留他的私有分支,直到它比诚实网络建立的分支更长,然后进行广播。

拥有超过 50%的算力,矿工可以撤销交易,阻止所有/一些交易被挖矿,阻止其他矿工的挖矿区块被插入到区块链中。

安定性

安定性是以太坊的下一个重大更新的名称。在撰写本书时,安定性仍在开发中。此更新将需要硬分叉。安定性将把共识协议改为 Casper,并将集成状态通道和分片。目前这些工作的完整细节还不清楚。让我们看一下这些是什么的高层概述。

支付通道和状态通道

在介绍状态通道之前,我们需要了解什么是支付通道。支付通道是一种功能,允许我们将发送以太币到另一个账户的超过两笔交易合并为两笔交易。它是如何工作的呢?假设 X 是一个视频流网站的所有者,Y 是一个用户。X 每分钟收取一以太币。现在 X 希望 Y 在观看视频的每分钟之后支付。当然,Y 可以每分钟广播一笔交易,但这里存在一些问题,比如 X 必须等待确认,所以视频将暂停一段时间,等等。这就是支付通道解决的问题。使用支付通道,Y 可以通过广播锁定交易将一些以太币(也许 100 以太币)锁定给 X 一段时间(也许 24 小时)。现在在观看 1 分钟视频后,Y 将发送一个签名记录,表明锁定可以解锁,并且一以太币将转到 X 的账户,其余将转到 Y 的账户。再过一分钟,Y 将发送一个签名记录,表明锁定可以解锁,并且两以太币将转到 X 的账户,其余将转到 Y 的账户。当 Y 在 X 的网站上观看视频时,这个过程将继续进行。现在一旦 Y 观看了 100 小时的视频或者 24 小时的时间即将到达,X 将向网络广播最终的签名记录以将资金提取到他的账户。如果 X 未能在 24 小时内提取,那么完全退款将转给 Y。因此,在区块链上,我们将只看到两笔交易:锁定和解锁。

支付通道用于与发送以太币相关的交易。类似地,状态通道允许我们合并与智能合约相关的交易。

股权证明和 Casper

在介绍 Casper 共识协议之前,我们需要了解股权证明共识协议是如何工作的。

股权证明是工作证明的最常见替代方案。工作证明浪费了太多计算资源。 POW 和 POS 的区别在于,在 POS 中,矿工不需要解决难题;相反,矿工需要证明拥有股份才能挖掘区块。 在 POS 系统中,帐户中的以太被视为股份,矿工挖掘区块的概率与矿工持有的股份成正比。 所以,如果矿工在网络中持有 10%的股份,它将挖掘 10%的区块。

但问题是我们怎么知道谁会挖掘下一个区块? 我们不能简单地让持有最高股份的矿工始终挖掘下一个区块,因为这将造成中心化。 有各种算法用于下一个区块的选择,例如随机化的区块选择和基于货币年龄的选择。

Casper 是 POS 的修改版本,解决了 POS 的各种问题。

划分

目前,每个节点都需要下载所有交易,这是庞大的。 随着区块链大小的增长速度,在未来几年内,下载整个区块链并将其同步将非常困难。

如果您熟悉分布式数据库架构,您一定熟悉划分。 如果不熟悉,那么划分是一种将数据分布在多台计算机上的方法。 以太坊将实现分片,以在节点之间分区和分布区块链。

您可以在github.com/ethereum/wiki/wiki/Sharding-FAQ了解更多关于对区块链进行划分的信息。

总结

在本章中,我们详细了解了以太坊的工作原理。 我们了解了区块时间如何影响安全性以及以太坊的弱点。 我们还了解了 Mist 和以太坊钱包是什么以及如何安装它们。 我们还看到了 geth 的一些重要命令。 最后,我们了解了以太坊 Serenity 更新中的新内容。

在下一章中,我们将学习有关存储和保护以太的各种方法。

第三章:撰写智能合约

在前一章中,我们学习了以太坊区块链的工作原理以及 PoW 共识协议如何保证其安全性。现在是时候开始撰写智能合约了,因为我们已经对以太坊的工作原理有了很好的把握。有各种各样的语言可以编写以太坊智能合约,但 Solidity 是最流行的。在本章中,我们将学习 Solidity 编程语言。最终,我们将构建一个用于在特定时间证明存在性、完整性和所有权的 DApp,即一个可以证明某个文件在特定时间与特定所有者在一起的 DApp。

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

  • Solidity 源文件的布局

  • 了解 Solidity 数据类型

  • 合约的特殊变量和函数

  • 控制结构

  • 合约的结构和特征

  • 编译和部署合约

Solidity 源文件

使用.sol扩展名指示 Solidity 源文件。与任何其他编程语言一样,Solidity 有各种版本。在撰写本书时,最新版本是 0.4.2。

在源文件中,你可以使用pragma Solidity指令提及编写代码所需的编译器版本。

例如,看一下以下示例:

pragma Solidity ⁰.4.2; 

现在,源文件将无法在早于版本 0.4.2 的编译器上编译,并且也无法在从版本 0.5.0 开始的编译器上工作(使用 ^ 添加了第二个条件)。版本在 0.4.2 到 0.5.0 之间的编译器最可能包含 bug 修复而不是任何破坏性更改。

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

智能合约的结构

合约类似于类。合约包含状态变量、函数、函数修饰符、事件、结构和枚举。合约还支持继承。继承是通过在编译时复制代码来实现的。智能合约还支持多态。

让我们看一个智能合约的示例,以了解其外观:

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保存了合约部署者的以太坊钱包地址,也就是合约部署的地址。

  • 然后,我们定义了一个事件。事件用于通知客户端有关某事的信息。每当data发生更改时,我们将触发此事件。所有事件都保存在区块链中。

  • 然后,我们定义了一个函数修饰符。修饰符用于在执行函数之前自动检查条件。在这里,修饰符检查合约的所有者是否调用了函数。如果没有,则会引发异常。

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

  • 然后,我们定义了两种方法。第一种方法是获取data状态变量的值,第二种方法是改变data的值。

在深入了解智能合约的特性之前,让我们先学习一些与 Solidity 相关的其他重要内容。 然后我们将回到合约。

数据位置

所有你之前学过的编程语言都是将它们的变量存储在内存中。而在 Solidity 中,变量根据上下文的不同,会被存储在内存和文件系统中。

根据上下文的不同,总是存在一个默认的位置。但是对于字符串、数组和结构等复杂数据类型,可以通过在类型后添加storagememory来覆盖默认位置。函数参数(包括返回参数)的默认位置是内存,局部变量的默认位置是存储,当然状态变量的位置强制为存储。

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

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

  • 对状态变量的赋值(即使来自其他状态变量)总是创建独立的副本。

  • 你不能将存储在内存中的复杂类型赋值给本地存储变量。

  • 在将状态变量赋值给本地存储变量时,本地存储变量指向状态变量;也就是说,本地存储变量成为了指针。

有哪些不同的数据类型?

Solidity 是一种静态类型语言;变量所持有的数据类型需要预先定义。默认情况下,所有变量的位都被赋值为 0。在 Solidity 中,变量是函数作用域的;也就是说,在函数内部声明的任何变量都将对整个函数的作用域有效,无论它在何处声明。

现在让我们来看看 Solidity 提供的各种数据类型:

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

  • uint8uint16uint24 ... uint256 用于分别存储 8 位、16 位、24 位 ... 256 位的无符号整数。同样,int8int16 ... int256 分别用于存储 8 位、16 位 ... 256 位的有符号整数。uintintuint256int256 的别名。类似于 uintintufixedfixed 用于表示小数。ufixed0x8ufixed0x16 ... ufixed0x256 用于分别存储 8 位、16 位 ... 256 位的无符号小数。同样,fixed0x8fixed0x16 ... fixed0x256 用于分别存储 8 位、16 位 ... 256 位有符号小数。如果一个数字需要多于 256 位的存储空间,那么就使用 256 位的数据类型,于此情况下将存储该数字的近似值。

  • address 用于通过分配十六进制文字来存储最多 20 个字节的值。它用于存储以太坊地址。address 类型公开两个属性:balancesendbalance 用于检查地址的余额,send 用于向地址转移以太币。send 方法接受需要转移的 wei 数量,并根据转移是否成功返回 true 或 false。wei 从调用 send 方法的合同中扣除。你可以在 Solidity 中使用 0x 前缀为变量分配十六进制编码的值。

数组

Solidity 支持通用数组和字节数组。它支持固定大小和动态数组。它还支持多维数组。

bytes1bytes2bytes3、...、bytes32 是字节数组的类型。bytebytes1 的别名。

这里是展示通用数组语法的示例:

contract sample{ 
    //dynamic size array 
    //wherever an array literal is seen a new array is created. If the array literal is in state than it's stored in storage and if it's found inside function than 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 therefore 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 属性,用于查找数组的长度。你也可以为 length 属性分配一个值来改变数组的大小。但是,在内存中无法调整数组的大小,也不能调整非动态数组的大小。

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

请记住,数组、结构体和映射都不能作为函数的参数,也不能作为函数的返回值。

字符串

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

这里是展示字符串语法的示例:

contract sample{ 
    //wherever a string literal is seen a new string is created. If the string literal is in state than it's stored in storage and if it's found inside function than 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 也支持结构体。以下是展示结构体语法的示例:

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 than it's stored in storage and if it's found inside function than its stored in memory 
    myStruct s2 = myStruct(true, ""); //struct method syntax 

    function sample(bool initBool, string initString){ 

        //create a instance of struct 
        s1 = myStruct(initBool, initString); 

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

请注意,函数参数不能是结构体,函数也不能返回结构体。

枚举

Solidity 也支持枚举。以下是展示枚举语法的示例:

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; 
    } 
} 

映射

映射数据类型是哈希表。映射只能存在于存储中,不能存在于内存中。因此,它们仅被声明为状态变量。映射可以被看作由键/值对组成。键实际上不存储;相反,使用键的 keccak256 哈希来查找值。映射没有长度。映射不能赋值给另一个映射。

这里是创建和使用映射的示例:

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; 
    } 
} 

请记住,如果尝试访问未设置的键,则会返回所有 0 位。

delete 操作符

delete 操作符可以应用于任何变量,将其重置为默认值。默认值是所有位分配为 0。

如果我们对动态数组应用 delete,那么它将删除所有元素,长度变为 0。如果对静态数组应用 delete,则所有索引将被重置。你也可以对特定索引应用 delete,在这种情况下,索引将被重置。

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

下面是一个演示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的类型也可以转换为address

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 支持ifelsewhileforbreakcontinuereturn? :控制结构。

下面是一个演示控制结构的例子:

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); 
    } 
} 

异常

在一些情况下,异常会自动抛出。您可以使用throw手动抛出异常。异常的效果是当前执行的调用被停止和回滚(也就是说,对状态和余额的所有更改都被撤销)。无法捕捉异常:

contract sample 
{ 
    function myFunction() 
    { 
        throw; 
    } 
} 

外部函数调用

在 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:公共函数和状态变量可以以所有可能的方式访问。编译器生成的访问器函数都是公共状态变量。你不能创建自己的访问器。实际上,它只生成 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之后的代码在调用者代码执行完成后执行。对于修饰符而言,在上一个修饰符的_;后的代码在调用者代码执行完成后执行。在前面的示例中,第 5、6 和 7 行永远不会被执行。在第 4 行之后,执行从第 8 行到第 10 行开始。

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

回退函数

一个合约可以有一个未命名的函数,称为fallback函数。这个函数不能有参数,也不能返回任何东西。如果没有其他函数匹配给定的函数标识符,它会在调用合约时执行。

当合约在没有任何函数调用的情况下接收以太时,也会执行这个函数;也就是说,交易向合约发送以太,并且不调用任何方法。在这样的情况下,通常很少有 gas 可用于函数调用(确切地说,只有 2,300 gas),因此很重要要尽量让 fallback 函数尽可能便宜。

接收以太但没有定义 fallback 函数的合约会抛出异常,将以太发送回去。因此,如果你希望你的合约接收以太,你必须实现一个 fallback 函数。

这里是一个 fallback 函数的例子:

contract sample 
{ 
    function() payable  
    { 
        //keep a note of how much Ether has been sent 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 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's in 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 doesn't call parent constructor instead parent is initialized and copied to child 
contract sample5 is sample3(122) 
{ 

} 

super 关键字

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合约的最终继承链是sample6sample5sample4sample2sample3sample1。继承链从最派生的合约开始,以最少派生的合约结束。

抽象合约

只包含函数原型而非实现的合约称为抽象合约。这样的合约不能被编译(即使它们包含了实现的函数和未实现的函数)。如果一个合约继承自一个抽象合约并且没有通过覆盖实现所有未实现的函数,那它本身就是抽象的。

这些抽象合约只是用来让编译器知道接口。当你引用已部署合约并调用它的函数时,这是有用的。

下面是一个用来示范这一点的例子:

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); 
    } 
} 

我们不能在合同源代码中添加库的地址;相反,在编译时需要将库地址提供给编译器。

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

  • 如果你有许多具有一些共同代码的合同,那么你可以将该共同代码部署为一个库。这样做可以节省 gas,因为 gas 取决于合同的大小。因此,我们可以将库视为使用它的合同的基本合同。使用基本合同而不是库来分割公共代码不会节省 gas,因为在 Solidity 中,继承是通过复制代码实现的。由于库被认为是基本合同的原因,库中具有内部可见性的函数会被复制到使用它的合同中;否则,具有库内部可见性的函数无法被使用库的合同调用,因为需要进行外部调用,并且具有内部可见性的函数无法使用外部调用调用。此外,库中的结构体和枚举将被复制到使用库的合同中。

  • 库可用于向数据类型添加成员函数。

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

使用 for

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

使用 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 there) into the current global scope. "filename" can be a absolute or relative path. It can only be a 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 (bytes): 完整的调用数据包括了事务调用的函数和其参数。

  • msg.gas (uint): 剩余燃气。

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

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

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

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

  • tx.gasprice (uint): 事务的燃气价格。

  • tx.origin (address): 事务的发送者(完整的调用链)。

与地址类型相关

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

  • <address>.balance (uint256): 以 wei 为单位的地址余额

  • <address>.send(uint256 amount) returns (bool): 向 address 发送指定数量的 wei; 失败时返回 false

与合约相关

合约相关的变量如下:

  • this: 当前合约,可显式转换为 address 类型。

  • selfdestruct(address recipient): 销毁当前合同,将其资金发送到给定地址。

以太单位

字面上的数字可以以 weifinneyszabo以太 为后缀,以在以太的子单位间转换,没有后缀的以太货币数被假定是 wei; 例如,2 Ether == 2000 finney 评估为 true

存在性、完整性和拥有权合约

让我们编写一份 Solidity 合约,它可以证明拥有文件的所有权,而不会显示实际文件。它可以证明文件在特定时间存在,并最终检查文件的完整性。

我们将通过将文件的哈希值和所有者的名字作为对存储来实现拥有权的证明。我们将通过将文件的哈希值和区块时间戳作为对存储来实现文件的存在性证明。最后,存储哈希本身证明了文件的完整性; 也就是说,如果文件被修改,那么它的哈希值将发生变化,合同将无法找到这样的文件,从而证明文件已经被修改。

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

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 or not 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 to the frontend that 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 编译器;相反,我们将使用 solcjs 和 Solidity 浏览器。Solcjs 允许我们在 Node.js 中以程序方式编译 Solidity,而浏览器 Solidity 是一个适用于小型合约的 IDE,它提供了编辑器并生成部署合约的代码。

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

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

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

您将获得以下输出:

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: '60606040526......,  
    gas: 4700000 
  }, 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 定义用于创建交易,因为它包含了所有方法的原型。

现在以开发者模式运行 geth,并启用挖矿。为此,请运行以下命令:

geth --dev --mine

现在打开另一个命令行窗口,在其中输入以下命令以打开 geth 的交互式 JavaScript 控制台:

geth attach

这将把 JS 控制台连接到另一个窗口中运行的 geth 实例。

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

合约地址是从其创建者的地址(from 地址)和创建者发送的交易数量(交易 nonce)确定性地计算出来的。这两个参数经过 RLP 编码,然后使用 keccak-256 散列算法进行哈希处理。我们将在后面更多地了解交易 nonce。您可以在 github.com/Ethereum/wiki/wiki/RLP 了解更多关于 RLP 的信息。

现在让我们存储文件的详细信息并检索它们。

放置此代码以广播交易以存储文件的详细信息:

var contract_obj = proofContract.at("0x9220c8ec6489a4298b06c2183cf04fb7e8fbd6d4"); 
contract_obj.set.sendTransaction("Owner Name", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", { 
 from: web3.eth.accounts[0], 
}, function(error, transactionHash){ 
  if (!err) 
    console.log(transactionHash); 
}) 

在这里,将合约地址替换为您获得的合约地址。proofContract.at 方法的第一个参数是合约地址。在这里,我们没有提供 gas,这种情况下,它会自动计算。

现在让我们找到文件的详细信息。按顺序运行此代码以查找文件的详细信息:

contract_obj.get.call("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); 

您将得到以下输出:

[1477591434, "Owner Name"] 

调用方法用于在当前状态下使用 EVM 调用合约的方法。它不广播交易。要读取数据,我们不需要广播,因为我们将拥有自己的区块链副本。

在接下来的章节中,我们将更多地了解 web3.js。

概要

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

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

第四章:使用 web3.js 入门

在上一章中,我们学习了如何编写智能合同,并使用 geth 的交互式控制台来部署和广播使用 web3.js 的交易。在本章中,我们将学习 web3.js 以及如何在 Node.js 或客户端 JavaScript 中导入、连接到 geth 并使用它。我们还将学习如何使用 web3.js 为前一章中创建的智能合同构建一个 web 客户端。

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

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

  • 连接到 geth

  • 探索使用 web3.js 可以完成的各种事情

  • 探索 web3.js 的各种最常用的 API

  • 构建一个用于所有权合同的 Node.js 应用程序

web3.js 简介

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

随着我们构建各种项目,你将越来越多地了解 web3.js,但现在,让我们先了解一些 web3.js 最常用的 API,然后我们将使用 web3.js 为我们的所有权智能合同构建一个前端。

撰写此文时,web3.js 的最新版本为 0.16.0。我们将学习与该版本相关的所有内容。

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

导入 web3.js

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

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

连接到节点

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

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

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

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

首先,我们在这里检查代码是否在 mist 中运行,方法是检查web3是否为undefined。如果web3已定义,则使用已有的实例;否则,通过连接到我们的自定义节点来创建一个实例。如果您想无论应用程序是否在 mist 中运行都连接到自定义节点,则从前面的代码中删除if条件。这里,我们假设我们的自定义节点在本地的端口号8545上运行。

Web3.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 提供了 API 来将 wei 余额转换为任何其他单位,并将任何其他单位的余额转换为 wei。

web3.fromWei()方法用于将 wei 数转换为任何其他单位,而web3.toWei()方法用于将其他单位中的数转换为 wei。这里有一个示例来演示这一点:

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

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

  • 千 wei/雅达

  • 兆 wei/巴贝吉

  • gwei/夏侬

  • 萨博

  • 芬尼

  • 以太币

  • 兆以太/拜尔/爱因斯坦

  • 以太

  • 获得

  • 泰尔

检索气价、余额和交易详情

让我们来看一下用于检索气价、地址余额以及已挖出交易信息的 API:

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

console.log(web3.eth.getBalance("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 45).toString()); 

console.log(web3.eth.getTransactionReceipt("0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b")); 

输出形式如下:

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

这里是前述方法的工作原理:

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

  • web3.ethgetBalance(): 返回任何给定地址的余额。所有哈希应作为十六进制字符串提供给 web3.js 的 API,而不是作为十六进制文本。对于 solidity 的address类型的输入,也应作为十六进制字符串提供。

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

    • blockHash: 交易所在块的哈希

    • blockNumber: 此交易所在块的块号

    • transactionHash: 交易的哈希

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

    • from: 发送者的地址

    • to: 接收者的地址;如果是一个合约创建交易,则为null

    • cumulativeGasUsed: 此交易执行时在块中使用的总的燃气量

    • gasUsed: 该特定交易独自使用的燃气量

    • contractAddress: 如果交易是一个合约创建的话,返回创建的合约地址;否则,返回 null

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

发送以太币

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

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

  • to:这是可选的。这是消息的目标地址,在合约创建交易中保持未定义。

  • value:这是可选的。这是以 wei 为单位的交易价值以及(如果是合约创建交易)赋予的资金。

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

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

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

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

让我们看一个向地址发送以太币的示例:

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

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

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

与合约交互

让我们学习如何部署新合约,通过地址获取已部署合约的引用,向合约发送以太币,发送调用合约方法的交易,并估算方法调用的 gas。

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

以下是创建合约对象的代码:

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"}]); 

一旦获取到合约,你可以使用合约对象的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。一旦合约被确认,address属性就会被设置。

proof合约中,没有构造函数,但如果有构造函数,则构造函数的参数应放在new方法的开头。我们传递的对象包含了来源地址、合约的字节码和使用的最大 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属性会被忽略。

如果你想在节点本身上调用方法而不是创建并广播交易,那么可以使用call而不是sendTransaction。以下示例演示了这一点:

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

有时候,我们需要找出调用一个方法所需要的 gas,以便决定是否进行调用。可以使用web3.eth.estimateGas来实现此目的。然而,直接使用web3.eth.estimateGas()需要你生成交易数据;因此,我们可以使用方法同名对象的estimateGas()方法。以下示例演示了这一点:

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

如果你只想向合约发送一些以太币而不调用任何方法,则可以简单地使用web3.eth.sendTransaction方法。

检索和监听合同事件

现在让我们看看如何监听合同的事件。监听事件非常重要,因为通过事务调用的方法的结果通常通过触发事件返回。

在我们深入研究如何检索和监听事件之前,我们需要学习事件的索引参数。事件的最多三个参数可以具有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)

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

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

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

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

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

  3. 然后,我们有合同实例的allEvents方法。它用于检索合同的所有事件。

  4. 每个事件都由一个包含以下属性的对象表示:

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

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

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

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

    • transactionHash:表示创建此日志的事务的哈希的字符串

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

    • blockHash:表示此日志所在块的哈希的字符串;当其处于挂起状态时为null

    • blockNumber:表示此日志所在块的块号;当其处于挂起状态时为null

web3.js 提供了一个web3.eth.filterAPI 来检索和监听事件。您可以使用此 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,这样后端就可以在每个等间隔的时间后将最近挖掘的交易推送到前端,而无需前端请求数据。

web3.js 可以在前端使用。但对于这个应用程序来说,这将是一个安全风险;也就是说,我们正在使用存储在 geth 中的帐户,并将 geth 节点 URL 暴露给前端,这将使这些帐户中的以太币处于风险之中。

项目结构

在本章的练习文件中,你会找到两个目录: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。如果您正在私有网络上运行 geth,则确保也启用了挖矿。最后,请确保帐户 0 存在并已解锁。您可以在启用了 rpc 和挖矿的私有网络上运行 geth,并且还解锁帐户 0:

geth --dev --mine --rpc --unlock=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; 
proof.set.sendTransaction(owner, fileHash, { 
from: web3.eth.accounts[0], 
}, 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); 
  } 
} 
}) 

在这里,我们检查状态是否为 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> 
                        <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 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> 

以下是代码的工作原理:

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

  2. 接下来,我们显示一个文本字段,用户可以输入所有者的详细信息。

  3. 然后,我们有两个按钮。第一个按钮用于将文件哈希和所有者的详细信息存储在合约中,第二个按钮用于从合约中获取文件的信息。单击提交按钮会触发submit()方法,而单击获取信息按钮会触发getInfo()方法。

  4. 然后,我们有一个警报框来显示消息。

  5. 最后,我们显示一个有序列表,显示用户在页面上的同时挖掘的合约的交易。

现在让我们为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 reader = new FileReader(); 
  reader.onload = function (event) { 
  var hash = sha1(event.target.result); 
  $.get("/submit?hash=" + hash + "&owner=" + owner, 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>"); 
} 
  }); 
}); 

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

  1. 首先,我们定义了submit()方法。在submit方法中,我们确保已选择文件并且文本字段不为空。然后,我们将文件内容读取为数组缓冲区,并将数组缓冲区传递给 sha1.js 公开的sha1()方法以获取数组缓冲区内部内容的哈希。一旦我们有了哈希,我们就使用 jQuery 发出 AJAX 请求到/submit路由,然后我们在警告框中显示交易哈希。

  2. 我们接下来定义getInfo()方法。首先确保已选择文件。然后,生成像之前生成的哈希一样的哈希,并向/getInfo端点发出请求以获取有关该文件的信息。

  3. 最后,我们使用socket.io库公开的io()方法建立了一个socket.io连接。然后,我们等待触发连接事件,这表明已建立连接。连接建立后,我们监听来自服务器的消息,并向用户显示有关交易的详细信息。

我们不将文件存储在以太坊区块链中,因为存储文件非常昂贵,需要大量的 gas。对于我们的情况,实际上我们不需要存储文件,因为网络中的节点将能够看到文件;因此,如果用户想要保密文件内容,那么他们将无法做到。我们应用程序的目的只是证明对文件的所有权,而不是像云服务一样存储和提供文件。

测试客户端

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

现在选择一个文件,输入所有者的姓名,然后单击提交。屏幕将更改为以下内容:

在这里,您可以看到交易哈希已显示。现在等待交易被挖掘。一旦交易被挖掘,您将能够在实时交易列表中看到该交易。屏幕将如下所示:

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

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

摘要

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

在下一章中,我们将构建一个钱包服务,用户可以轻松创建和管理以太坊钱包,而且还可以离线进行。我们将专门使用 LightWallet 库来实现这一目标。

第五章:构建钱包服务

钱包服务用于发送和接收资金。构建钱包服务的主要挑战是安全性和信任。用户必须感到他们的资金是安全的,并且钱包服务的管理员不会窃取他们的资金。我们将在本章中构建的钱包服务将解决这两个问题。

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

  • 在线和离线钱包的区别

  • 使用 hooked-web3-provider 和 ethereumjs-tx 可以更轻松地创建和签署使用由以太坊节点管理的帐户以外的帐户的交易。

  • 理解 HD 钱包以及其用途。

  • 使用 lightwallet.js 创建 HD 钱包和交易签名器

  • 构建钱包服务

在线和离线钱包的区别

钱包是帐户的集合,而帐户是地址及其关联私钥的组合。

当钱包连接到互联网时,称为在线钱包。例如,存储在 geth 中的钱包、任何网站/数据库等称为在线钱包。在线钱包也称为热钱包、网页钱包、托管钱包等。至少在存储大量以太币或长时间存储以太币时,不建议使用在线钱包,因为它们存在风险。此外,根据钱包存储的位置不同,可能需要信任第三方。

例如,大多数流行的钱包服务都会将钱包的私钥与自己存储,并允许您通过电子邮件和密码访问钱包,因此基本上您无法真正访问钱包,如果他们愿意,他们可以窃取钱包中的资金。

当钱包未连接到互联网时,称为离线钱包。例如,存储在闪存驱动器、纸张、文本文件等中的钱包。离线钱包也称为冷钱包。离线钱包比在线钱包更安全,因为要窃取资金,某人需要对存储物理接触。离线存储的挑战在于您需要找到一个您不会意外删除或忘记的位置,或者其他人无法访问。许多人将钱包存储在纸上,并将纸放在保险柜中,如果他们想要长期安全地保留一些资金。如果您想经常从您的帐户发送资金,那么您可以将其存储在受密码保护的闪存驱动器中,并且还可以放在安全柜中。仅在数字设备中存储钱包有一点风险,因为数字设备随时可能损坏,您可能无法访问您的钱包;因此,除了存储在闪存驱动器中,您还应该将其放在安全柜中。根据您的需求,您还可以找到更好的解决方案,但请确保它是安全的,并且不要意外丢失对其的访问权限。

hooked-web3-provider 和 ethereumjs-tx 库

到目前为止,我们看到的 Web3.js 库的 sendTransaction() 方法的所有示例都是使用 Ethereum 节点中存在的 from 地址;因此,在广播之前以太坊节点能够签署交易。但是如果你有钱包的私钥存储在其他地方,那么 geth 就无法找到它。因此,在这种情况下,您需要使用 web3.eth.sendRawTransaction() 方法来广播交易。

web3.eth.sendRawTransaction() 用于广播原始交易,也就是说,您将不得不编写代码来创建和签署原始交易。以太坊节点将直接广播它,而不对交易进行其他处理。但是编写使用 web3.eth.sendRawTransaction() 广播交易的代码很困难,因为它需要生成数据部分、创建原始交易,还要对交易进行签名。

Hooked-Web3-Provider 库为我们提供了一个自定义的提供程序,它使用 HTTP 与 geth 进行通信;但这个提供程序的独特之处在于,它允许我们使用我们的密钥对合约实例的 sendTransaction() 调用进行签名。因此,我们不再需要创建交易的数据部分。这个自定义提供程序实际上覆盖了 web3.eth.sendTransaction() 方法的实现。所以基本上,它允许我们对合约实例的 sendTransaction() 调用以及 web3.eth.sendTransaction() 调用进行签名。合约实例的 sendTransaction() 方法内部生成交易的数据,并调用 web3.eth.sendTransaction() 来广播交易。

EthereumJS 是与以太坊相关的那些库的集合。ethereumjs-tx 是其中之一,提供了与交易相关的各种 API。例如,它允许我们创建原始交易、签署原始交易、检查交易是否使用正确的密钥签名等。

这两个库都适用于 Node.js 和客户端 JavaScript。从 www.npmjs.com/package/hooked-web3-provider 下载 Hooked-Web3-Provider,从 www.npmjs.com/package/ethereumjs-tx 下载 ethereumjs-tx。

在编写本书时,Hooked-Web3-Provider 的最新版本是 1.0.0,ethereumjs-tx 的最新版本是 1.1.4。

让我们看看如何将这些库一起使用,从一个不由 geth 管理的账户发送交易。

var provider = new HookedWeb3Provider({ 
 host: "http://localhost:8545", 
 transaction_signer: { 
  hasAddress: function(address, callback){ 
   callback(null, true); 
  }, 
  signTransaction: function(tx_params, callback){ 
   var rawTx = { 
          gasPrice: web3.toHex(tx_params.gasPrice), 
          gasLimit: web3.toHex(tx_params.gas), 
          value: web3.toHex(tx_params.value) 
          from: tx_params.from, 
          to: tx_params.to, 
          nonce: web3.toHex(tx_params.nonce) 
      }; 

   var privateKey = EthJS.Util.toBuffer('0x1a56e47492bf3df9c9563fa7f66e4e032c661de9d68c3f36f358e6bc9a9f69f2', 'hex'); 
   var tx = new EthJS.Tx(rawTx); 
   tx.sign(privateKey); 

   callback(null, tx.serialize().toString('hex')); 
  } 
 } 
}); 

var web3 = new Web3(provider); 

web3.eth.sendTransaction({ 
 from: "0xba6406ddf8817620393ab1310ab4d0c2deda714d", 
 to: "0x2bdbec0ccd70307a00c66de02789e394c2c7d549", 
 value: web3.toWei("0.1", "ether"), 
 gasPrice: "20000000000", 
 gas: "21000" 
}, function(error, result){ 
 console.log(error, result) 
})

以下是代码的工作原理:

  1. 首先,我们创建了一个 HookedWeb3Provider 实例。这由 Hooked-Web3-Provider 库提供。这个构造函数接受一个对象,该对象必须提供两个属性。host 是节点的 HTTP URL,transaction_signer 是一个与自定义提供程序通信以获取交易签名的对象。

  2. transaction_signer对象有两个属性:hasAddresssignTransactionhasAddress被调用来检查交易是否可以签名,也就是检查交易签名者是否拥有from地址账户的私钥。该方法接收地址和一个回调函数。回调函数应该以错误消息作为第一个参数调用,而私钥如果未找到则应为false作为第二个参数。如果找到了私钥,那么第一个参数应该为null,第二个参数应为true

  3. 如果找到了地址的私钥,那么自定义提供者将调用signTransaction方法来对交易进行签名。该方法有两个参数,即交易参数和一个回调。在方法内部,首先,我们将交易参数转换为原始交易参数,也就是将原始交易参数值编码为十六进制字符串。然后,我们创建一个缓冲区来保存私钥。该缓冲区是使用EthJS.Util.toBuffer()方法创建的,该方法是ethereumjs-util库的一部分。ethereumjs-util库是ethereumjs-tx库导入的。然后我们创建一个原始交易并对其进行签名,之后我们对其进行序列化并转换为十六进制字符串。最后,我们需要使用回调将签名的原始交易的十六进制字符串提供给自定义提供者。如果方法内部有错误,则回调的第一个参数应该是一个错误消息。

  4. 现在自定义提供者获取原始交易并使用web3.eth.sendRawTransaction()来广播它。

  5. 最后,我们调用web3.eth.sendTransaction函数向另一个账户发送一些以太币。在这里,我们需要提供除了nonce之外的所有交易参数,因为自定义提供者可以计算 nonce。之前,许多这些都是可选的,因为我们让以太坊节点来计算它们,但现在当我们自己签名时,我们需要提供所有这些。当交易没有任何与之关联的数据时,gas始终为 21,000。

公钥呢?

在上面的代码中,我们从未提及签名地址的公钥。你一定想知道矿工如何在没有公钥的情况下验证交易的真实性。矿工使用 ECDSA 的一个独特属性,该属性允许您从消息和签名中计算公钥。在交易中,消息指示了交易的意图,而签名用于确定消息是否使用了正确的私钥进行签名。这就是使 ECDSA 如此特殊的地方。ethereumjs-tx 提供了一个 API 来验证交易。

什么是分层确定性钱包?

分层确定性钱包是从称为种子的单个起始点派生地址和密钥的系统。 确定性意味着对于相同的种子,将生成相同的地址和密钥,并且分层意味着将以相同的顺序生成地址和密钥。 这使得更容易备份和存储多个帐户,因为您只需要存储种子,而不需要存储单独的密钥和地址。

为什么用户需要多个帐户?

您可能想知道为什么用户需要多个帐户。 原因是为了隐藏他们的财富。 帐户的余额是公开可见的。 因此,如果用户 A 与用户 B 共享一个地址来接收一些以太币,那么用户 B 可以查看该地址中有多少以太币。 因此,用户通常会在各种帐户之间分配他们的财富。

有各种类型的 HD 钱包,它们在种子格式和生成地址和密钥的算法方面有所不同,例如 BIP32、Armory、Coinkite、Coinb.in 等。

什么是 BIP32、BIP44 和 BIP39?

比特币改进提案BIP)是一份向比特币社区提供信息的设计文件,或者描述比特币或其流程或环境的新功能。 BIP 应该提供对该功能的简明技术规范和功能的基本原理。 在撰写本书时,有 152 个 BIPS(比特币改进提案)。 BIP32 和 BIP39 分别提供了有关实现 HD 钱包和助记种子规范的算法的信息。 您可以在github.com/bitcoin/bips上了解更多信息。

导出密钥的功能简介

非对称加密算法定义了其密钥的性质以及如何生成密钥,因为密钥需要相关联。 例如,RSA 密钥生成算法是确定性的。

对称加密算法仅定义密钥大小。 生成密钥交给我们。 有各种算法来生成这些密钥。 这样的算法之一是 KDF。

密钥派生函数KDF)是一种确定性算法,用于从某个秘密值(例如主密钥、密码或口令)派生对称密钥。 有各种类型的 KDF,例如 bcrypt、crypt、PBKDF2、scrypt、HKDF 等。 您可以在en.wikipedia.org/wiki/Key_derivation_function上了解更多关于 KDF 的信息。

要从单个秘密值生成多个密钥,可以连接一个数字并递增它。

基于密码的密钥派生函数接受一个密码并生成对称密钥。由于用户通常使用弱密码,基于密码的密钥派生函数被设计为更慢且占用大量内存,以使启动暴力攻击和其他类型的攻击变得困难。基于密码的密钥派生函数被广泛使用,因为很难记住秘密密钥,将它们存储在某处是有风险的,因为它可能被窃取。PBKDF2 是基于密码的密钥派生函数的一个例子。

主密钥或密码短语很难通过暴力攻击破解;因此,如果您想要从主密钥或密码短语生成对称密钥,可以使用非基于密码的密钥派生函数,例如 HKDF。与 PBKDF2 相比,HKDF 要快得多。

为什么不直接使用散列函数而不是 KDFs?

散列函数的输出可以用作对称密钥。那么你一定会想到为什么需要 KDFs。嗯,如果您使用的是主密钥、密码短语或强密码,您可以简单地使用散列函数。例如,HKDF 简单地使用散列函数生成密钥。但如果不能保证用户将使用强密码,最好使用基于密码的散列函数。

LightWallet 介绍

LightWallet 是一个实现了 BIP32、BIP39 和 BIP44 的 HD 钱包。LightWallet 提供了使用其生成的地址和密钥创建和签名交易或加密和解密数据的 API。

LightWallet API 分为四个命名空间,即 keystoresigningencryptiontxutilssigningencryptiontxutils 分别提供签署交易、非对称加密和创建交易的 API,而 keystore 命名空间用于创建 keystore、生成的种子等。keystore 是一个保存种子和加密密钥的对象。如果我们使用 Hooked-Web3-Provider,keystore 命名空间实现了需要签署 we3.eth.sendTransaction() 调用的交易签名方法。因此,keystore 命名空间可以自动为其中找到的地址创建和签署交易。实际上,LightWallet 主要用于成为 Hooked-Web3-Provider 的签名提供者。

keystore 实例可以配置为创建和签名交易或加密和解密数据。对于签名交易,它使用 secp256k1 参数,而对于加密和解密,它使用 curve25519 参数。

LightWallet 的种子是一个包含 12 个单词的助记词,易于记忆但难以破解。这不是任意的 12 个单词;相反,它应该是由 LightWallet 生成的种子。由 LightWallet 生成的种子在选择单词和其他方面具有特定属性。

HD 派生路径

HD 派生路径是一个字符串,它使得易于处理多个加密货币(假设它们都使用相同的签名算法)、多个区块链、多个账户等等。

HD 派生路径可以具有任意多个参数,并且可以使用不同值的参数,我们可以产生不同组的地址及其相关密钥。

LightWallet 默认使用m/0'/0'/0'派生路径。这里,/n'是一个参数,n是参数值。

每个 HD 派生路径都有一个curvepurposepurpose可以是signasymEncryptsign表示该路径用于签署交易,而asymEncrypt表示该路径用于加密和解密。curve指示 ECC 的参数。对于签名,参数必须是secp256k1,对于非对称加密,曲线必须是curve25591,因为 LightWallet 强制我们使用这些参数,由于其在这些用途上的好处。

构建钱包服务

现在我们已经学习了足够多关于 LightWallet 的理论知识,是时候使用 LightWallet 和 hooked-web3-provider 构建一个钱包服务。我们的钱包服务将允许用户生成唯一的种子,显示地址及其关联余额,最后,该服务将允许用户向其他账户发送以太币。所有操作都将在客户端完成,这样用户就可以轻松地信任我们。用户要么必须记住种子,要么将其存放在某处。

先决条件

在开始构建钱包服务之前,请确保您正在运行开采的 geth 开发实例,已启用 HTTP-RPC 服务器,允许来自任何域的客户端请求,并最终已解锁账户 0。您可以通过运行以下命令来做到这一切:

    geth --dev --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545" --mine --unlock=0

在这里,--rpccorsdomain用于允许特定域与 geth 通信。我们需要提供一个用空格分隔的域列表,例如"http://localhost:8080 https://mySite.com *"。它也支持*通配符字符。--rpcaddr表示 geth 服务器可到达的 IP 地址。默认值为127.0.0.1,因此如果它是托管服务器,您将无法使用服务器的公共 IP 地址来访问它。因此,我们将其值更改为0.0.0.0,表示服务器可以使用任何 IP 地址访问。

项目结构

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

要测试Final目录,您需要在其中运行npm install,然后使用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目录中,您将找到用于 Hooked-Web3-Provider、web3js 和 LightWallet 的.js文件。在public/js中,您还将找到一个main.js文件,其中您将放置我们应用的前端 JS 代码。

构建后端

让我们首先构建应用程序的后端。首先,在初始目录中运行npm install来安装我们后端所需的依赖项。

这是运行 express 服务并提供index.html文件和静态文件的完整后端代码:

var express = require("express");   
var app = express();   

app.use(express.static("public")); 

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

app.listen(8080);

上述代码是不言自明的。

构建前端

现在让我们构建应用的前端。前端将包括主要功能,即生成种子、显示种子地址和发送以太。

现在让我们编写应用程序的 HTML 代码。将此代码放入index.html文件中:

<!DOCTYPE html> 
 <html lang="en"> 
     <head> 
         <meta charset="utf-8"> 
         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
         <meta http-equiv="x-ua-compatible" content="ie=edge"> 
         <link rel="stylesheet" href="/css/bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <div class="row"> 
                 <div class="col-md-6 offset-md-3"> 
                     <br> 
                     <div class="alert alert-info" id="info" role="alert"> 
                           Create or use your existing wallet. 
                     </div> 
                     <form> 
                         <div class="form-group"> 
                             <label for="seed">Enter 12-word seed</label> 
                             <input type="text" class="form-control" id="seed"> 
                         </div> 
                         <button type="button" class="btn btn-primary" onclick="generate_addresses()">Generate Details</button> 
                         <button type="button" class="btn btn-primary" onclick="generate_seed()">Generate New Seed</button> 
                     </form> 
                     <hr> 
                     <h2 class="text-xs-center">Address, Keys and Balances of the seed</h2> 
                     <ol id="list"> 
                     </ol> 
                     <hr> 
                     <h2 class="text-xs-center">Send ether</h2> 
                     <form> 
                         <div class="form-group"> 
                             <label for="address1">From address</label> 
                             <input type="text" class="form-control" id="address1"> 
                         </div> 
                         <div class="form-group"> 
                             <label for="address2">To address</label> 
                             <input type="text" class="form-control" id="address2"> 
                         </div> 
                         <div class="form-group"> 
                             <label for="ether">Ether</label> 
                             <input type="text" class="form-control" id="ether"> 
                         </div> 
                         <button type="button" class="btn btn-primary" onclick="send_ether()">Send Ether</button> 
                     </form> 
                 </div> 
             </div> 
         </div> 

            <script src="img/web3.min.js"></script> 
            <script src="img/hooked-web3-provider.min.js"></script> 
         <script src="img/lightwallet.min.js"></script> 
         <script src="img/main.js"></script> 
     </body> 
 </html>

代码的工作原理如下:

  1. 首先,我们将一个 Bootstrap 4 样式表加入队列。

  2. 然后我们显示一个信息框,我们将在其中向用户显示各种消息。

  3. 然后我们有一个带有输入框和两个按钮的表单。输入框用于输入种子,或者在生成新种子时,在那里显示种子。

  4. 生成详细信息按钮用于显示地址,而生成新种子按钮用于生成新的唯一种子。当点击生成详细信息时,我们调用generate_addresses()方法,当点击生成新种子按钮时,我们调用generate_seed()方法。

  5. 稍后,我们有一个空的有序列表。在这里,当用户单击生成详细信息按钮时,我们将动态显示种子的地址、余额和关联的私钥。

  6. 最后,我们有另一种形式,需要一个发送地址和一个接收地址以及要转移的以太数量。发送地址必须是当前在无序列表中显示的地址之一。

现在让我们编写 HTML 代码调用每个函数的实现。首先,让我们编写代码以生成新种子。将此代码放入main.js文件中:

function generate_seed() 
{ 
 var new_seed = lightwallet.keystore.generateRandomSeed(); 

 document.getElementById("seed").value = new_seed; 

 generate_addresses(new_seed); 
}

keystore命名空间的generateRandomSeed()方法用于生成随机种子。它接受一个可选参数,该参数是一个指示额外熵的字符串。

熵是应用程序收集用于某些算法或其他需要随机数据的地方的随机性。通常,熵是从硬件源收集的,可以是现有的源,例如鼠标移动或专门提供的随机性生成器。

要生成唯一的种子,我们需要真正的高熵。LightWallet 已经内置了产生唯一种子的方法。LightWallet 用于生成熵的算法取决于环境。但是,如果您认为可以生成更好的熵,则可以将生成的熵传递给generateRandomSeed()方法,并且它将与内部由generateRandomSeed()生成的熵连接在一起。

生成随机种子后,我们调用generate_addresses方法。此方法接受一个种子并在其中显示地址。在生成地址之前,它会提示用户询问他们想要多少地址。

以下是generate_addresses()方法的实现。将此代码放入main.js文件中:

var totalAddresses = 0; 

function generate_addresses(seed) 
{ 
 if(seed == undefined) 
 { 
  seed = document.getElementById("seed").value; 
 } 

 if(!lightwallet.keystore.isSeedValid(seed)) 
 { 
  document.getElementById("info").innerHTML = "Please enter a valid seed"; 
  return; 
 } 

 totalAddresses = prompt("How many addresses do you want to generate"); 

 if(!Number.isInteger(parseInt(totalAddresses))) 
 { 
  document.getElementById("info").innerHTML = "Please enter valid number of addresses"; 
  return; 
 } 

 var password = Math.random().toString(); 

 lightwallet.keystore.createVault({ 
  password: password, 
    seedPhrase: seed 
 }, function (err, ks) { 
    ks.keyFromPassword(password, function (err, pwDerivedKey) { 
      if(err) 
      { 
       document.getElementById("info").innerHTML = err; 
      } 
      else 
      { 
       ks.generateNewAddress(pwDerivedKey, totalAddresses); 
       var addresses = ks.getAddresses();  

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

       var html = ""; 

       for(var count = 0; count < addresses.length; count++) 
       { 
     var address = addresses[count]; 
     var private_key = ks.exportPrivateKey(address, pwDerivedKey); 
     var balance = web3.eth.getBalance("0x" + address); 

     html = html + "<li>"; 
     html = html + "<p><b>Address: </b>0x" + address + "</p>"; 
     html = html + "<p><b>Private Key: </b>0x" + private_key + "</p>"; 
     html = html + "<p><b>Balance: </b>" + web3.fromWei(balance, "ether") + " ether</p>"; 
        html = html + "</li>"; 
       } 

       document.getElementById("list").innerHTML = html; 
      } 
    }); 
 }); 
}

以下是代码的工作原理:

  1. 首先,我们有一个名为totalAddresses的变量,其中包含一个数字,表示用户想要生成的地址总数。

  2. 然后我们检查seed参数是否已定义。如果未定义,则从输入字段获取种子。我们这样做是为了在生成新种子时以及用户单击生成详细信息按钮时,可以使用generate_addresses()方法来显示信息种子。

  3. 然后我们使用keystore命名空间的isSeedValid()方法验证种子。

  4. 然后我们要求用户输入有关他们想要生成和显示多少地址的输入。然后我们验证输入。

  5. keystore命名空间中的私钥始终以加密形式存储。在生成密钥时,我们需要对其进行加密,而在签署交易时,我们需要解密密钥。从用户处获取对称加密密钥的密码,或通过提供随机字符串作为密码来获得。为了提供更好的用户体验,我们生成一个随机字符串并将其用作密码。对称密钥不存储在keystore命名空间内;因此,我们需要在执行与私钥相关的操作时(如生成密钥、访问密钥等),从密码生成密钥。

  6. 然后我们使用createVault方法创建一个keystore实例。createVault接受一个对象和一个回调函数。对象可以有四个属性:passwordseedPharsesalthdPathStringpassword是必需的,其他全部是可选的。如果我们不提供seedPharse,它会生成并使用一个随机种子。salt被连接到密码上,以增加对称密钥的安全性,因为攻击者还必须找到盐和密码。如果未提供盐,它将被随机生成。keystore命名空间保存未加密的盐。hdPathString用于为keystore命名空间提供默认派生路径,即在生成地址、签署交易等中使用。如果我们不提供派生路径,将使用此派生路径。如果我们不提供hdPathString,则默认值为m/0'/0'/0'。此派生路径的默认目的是sign。您可以使用keystore实例的addHdDerivationPath()方法创建新的派生路径或覆盖现有派生路径的目的。您也可以使用keystore实例的setDefaultHdDerivationPath()方法更改默认派生路径。最后,一旦创建了keystore命名空间,实例会通过回调返回。因此,在这里,我们只使用密码和种子创建了一个keystore

  7. 现在我们需要生成用户需要的地址数量及其关联的密钥。因为我们可以从种子生成数百万个地址,所以keystore在我们需要它之前不会生成任何地址,因为它不知道我们要生成多少个地址。创建了keystore之后,我们使用keyFromPassword方法从密码生成对称密钥。然后我们调用generateNewAddress()方法生成地址及其关联的密钥。

  8. generateNewAddress()方法接受三个参数:从密码派生的密钥、要生成的地址数和派生路径。由于我们没有提供派生路径,它使用keystore的默认派生路径。如果多次调用generateNewAddress(),它将从上一次创建的地址处继续。例如,如果两次调用这个方法,每次生成两个地址,你将得到前四个地址。

  9. 然后我们使用getAddresses()来获取keystore中存储的所有地址。

  10. 我们使用exportPrivateKey方法解密并检索地址的私钥。

  11. 我们使用web3.eth.getBalance()来获取地址的余额。

  12. 最后,我们在无序列表中显示所有信息。

现在我们知道如何从种子生成地址及其私钥了。现在让我们编写send_ether()方法的实现,该方法用于从种子生成的地址中发送以太币。

这是用于此的代码。将此代码放入main.js文件中:

function send_ether() 
{ 
 var seed = document.getElementById("seed").value; 

 if(!lightwallet.keystore.isSeedValid(seed)) 
 { 
  document.getElementById("info").innerHTML = "Please enter a valid seed"; 
  return; 
 } 

 var password = Math.random().toString(); 

 lightwallet.keystore.createVault({ 
  password: password, 
    seedPhrase: seed 
 }, function (err, ks) { 
    ks.keyFromPassword(password, function (err, pwDerivedKey) { 
      if(err) 
      { 
       document.getElementById("info").innerHTML = err; 
      } 
      else 
      { 
       ks.generateNewAddress(pwDerivedKey, totalAddresses); 

       ks.passwordProvider = function (callback) { 
          callback(null, password); 
       }; 

       var provider = new HookedWeb3Provider({ 
       host: "http://localhost:8545", 
       transaction_signer: ks 
    }); 

       var web3 = new Web3(provider); 

       var from = document.getElementById("address1").value; 
    var to = document.getElementById("address2").value; 
       var value = web3.toWei(document.getElementById("ether").value, "ether"); 

       web3.eth.sendTransaction({ 
        from: from, 
        to: to, 
        value: value, 
        gas: 21000 
       }, function(error, result){ 
        if(error) 
        {  
         document.getElementById("info").innerHTML = error; 
        } 
        else 
        { 
         document.getElementById("info").innerHTML = "Txn hash: " + result; 
        } 
       }) 
      } 
    }); 
 }); 
}

到生成地址种子的代码部分为止都是自说明的。之后,我们将一个回调分配给kspasswordProvider属性。在交易签名期间,此回调会被调用以获取解密私钥所需的密码。如果我们不提供此回调,LightWallet 会提示用户输入密码。然后,我们通过将keystore作为交易签名者传递来创建一个HookedWeb3Provider实例。现在,当自定义提供程序想要签署交易时,它会调用kshasAddresssignTransactions方法。如果要签署的地址不在生成的地址列表中,ks将向自定义提供程序返回错误。最后,我们使用web3.eth.sendTransaction方法发送一些以太币。

测试中

现在我们已经完成了建立钱包服务的工作,让我们来测试一下以确保它按预期运行。首先,在初始目录中运行node app.js,然后在喜爱的浏览器中访问http://localhost:8080。你会看到这个屏幕:

现在点击"Generate New Seed"按钮生成一个新种子。你将被提示输入表示要生成的地址数量的数字。你可以提供任意数字,但出于测试目的,提供一个大于 1 的数字。现在屏幕会看起来像这样:

现在要测试发送以太币,你需要从 coinbase 账户向生成的地址中的一个发送一些以太币。一旦你向其中一个生成的地址发送了一些以太币,点击"Generate Details"按钮刷新界面,尽管用钱包服务发送以太币并不需要这样做。确保再次生成相同的地址。现在屏幕会看起来如下:

现在,在"from"地址字段中输入列表中具有余额的账户地址。然后在"to"地址字段中输入另一个地址。为了测试目的,你可以输入显示的其他任何地址。然后输入小于或等于"from"地址账户的以太坊余额的一些以太币数量。现在你的屏幕会看起来如下:

现在点击"Send Ether"按钮,你会在信息框中看到交易哈希。等待一段时间让它被挖掘。同时,你可以通过在非常短的时间内点击"Generate Details"按钮来检查交易是否已被挖掘。一旦交易被挖掘,你的屏幕将看起来像这样:

如果一切按照说明的方式进行,你的钱包服务就已经准备好了。你可以将这个服务部署到自定义域名上,并让它对外提供使用。它是完全安全的,用户会信任它。

摘要

在本章中,你学习了三个重要的以太坊库:Hooked-Web3-Provider、ethereumjs-tx 和 LightWallet。这些库可用于在以太坊节点之外管理账户并签署交易。在开发大多数类型的 DApp 客户端时,你会发现这些库非常有用。

最后,我们创建了一个钱包服务,让用户可以管理与服务后端共享私钥或与其钱包相关的任何其他信息的账户。

在下一章中,我们将构建一个平台来构建和部署智能合约。

第六章:构建智能合约部署平台

一些客户端可能需要在运行时编译和部署合约。在我们的拥有权 DApp 中,我们手动部署了智能合约,并在客户端代码中硬编码了合约地址。但是一些客户端可能需要在运行时部署智能合约。例如,如果客户端允许学校在区块链中记录学生的出勤情况,那么每次注册新学校时都需要部署一个智能合约,以便每个学校完全控制其智能合约。在本章中,我们将学习如何使用 web3.js 编译智能合约,并使用 web3.js 和 EthereumJS 部署它。

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

  • 计算交易的 nonce

  • 使用交易池 JSON-RPC API

  • 生成用于合约创建和方法调用的交易数据

  • 估算交易所需的 gas

  • 查找账户的当前可花费余额

  • 使用 solcjs 编译智能合约

  • 开发一个平台来编写、编译和部署智能合约

计算交易的 nonce

对于由 geth 维护的账户,我们不需要担心交易 nonce,因为 geth 可以为交易添加正确的 nonce 并对其进行签名。当使用 geth 不管理的账户时,我们需要自己计算 nonce。

要自己计算 nonce,我们可以使用 geth 提供的 getTransactionCount 方法。第一个参数应该是我们需要交易计数的地址,第二个参数是我们需要交易计数直到的区块。我们可以提供 "pending" 字符串作为区块,以包括当前正在挖掘的区块中的交易。正如我们在之前的章节中讨论的那样,geth 维护一个交易池,其中包含挂起和排队的交易。为了挖掘一个区块,geth 从交易池中取出挂起的交易并开始挖掘新的区块。直到区块没有被挖掘,挂起的交易仍然保留在交易池中,一旦被挖掘,已挖掘的交易就会从交易池中删除。在挖掘一个区块时接收到的新进交易被放入交易池,并在下一个区块中被挖掘。因此,当我们在调用 getTransactionCount 时提供 "pending" 作为第二个参数时,它不会查看交易池内部;相反,它只考虑挂起区块中的交易。

因此,如果您试图从 geth 不管理的账户发送交易,则需计算区块链中账户的总交易数,并将其与交易池中挂起的交易相加。如果尝试使用挂起区块中的挂起交易,则在几秒钟的间隔内发送交易到 geth,您将无法获得正确的 nonce,因为平均需要 12 秒才能将交易包含在区块链中。

在上一章中,我们依赖hooked-web3-provider来为交易添加 nonce。不幸的是,hooked-web3-provider没有以正确的方式获取 nonce。它为每个账户维护一个计数器,并在你从该账户发送交易时递增该计数器。如果交易无效(例如,如果交易尝试发送的以太币多于它拥有的),则不会递减计数器。因此,该账户的其余交易将排队,并永远不会被挖掘,直到hooked-web3-provider被重置,即客户端被重新启动。如果你创建多个hooked-web3-provider实例,那么这些实例无法将一个账户的 nonce 与彼此同步,因此你可能会得到不正确的 nonce。但在你将 nonce 添加到交易之前,hooked-web3-provider总是获取到 pending 区块的交易计数,并将其与其计数器进行比较,并使用较大者。因此,如果由hooked-web3-provider管理的账户的交易是从网络中的另一个节点发送的,并包含在 pending 区块中,则hooked-web3-provider可以看到它。但总的来说,不能依赖hooked-web3-provider来计算 nonce。它非常适用于快速原型设计客户端应用程序,并适用于在用户可以看到并重新发送交易(如果它们未广播到网络并且hooked-web3-provider经常被重置)的应用程序中使用。例如,在我们的钱包服务中,用户会频繁加载页面,因此会频繁创建新的hooked-web3-provider实例。如果交易未被广播、无效或未被挖掘,那么用户可以刷新页面并重新发送交易。

介绍 solcjs

solcjs 是一个用于编译 solidity 文件的 Node.js 库和命令行工具。它不使用 solc 命令行编译器;相反,它纯粹使用 JavaScript 进行编译,因此比 solc 更容易安装。

Solc 是实际的 Solidity 编译器。Solc 是用 C++ 编写的。这个 C++ 代码使用 emscripten 编译成 JavaScript。每个 solc 版本都被编译成 JavaScript。在 github.com/ethereum/solc-bin/tree/gh-pages/bin,你可以找到每个 solidity 版本的基于 JavaScript 的编译器。solcjs只是使用这些基于 JavaScript 的编译器之一来编译 solidity 源代码。这些基于 JavaScript 的编译器可以在浏览器和 Node.js 环境中运行。

浏览器 Solidity 使用这些基于 JavaScript 的编译器来编译 Solidity 源代码。

安装 solcjs

solcjs 作为一个名为 solc 的 npm 包可供使用。你可以像安装其他任何 npm 包一样,本地或全局安装 solcjs npm 包。如果全局安装了此包,则将可用一个名为 solcjs 的命令行工具。因此,为了安装命令行工具,请运行此命令:

    npm install -g solc

现在,请继续运行此命令以查看如何使用命令行编译器编译 Solidity 文件:

    solcjs -help

我们不会探索 solcjs 命令行工具;而是学习如何使用 solcjs API 来编译 Solidity 文件。

默认情况下,solcjs 将使用匹配其版本的编译器版本。例如,如果您安装了 solcjs 版本 0.4.8,则默认情况下将使用 0.4.8 编译器版本进行编译。solcjs 还可以配置为使用其他编译器版本。在撰写本文时,solcjs 的最新版本是 0.4.8。

solcjs API

solcjs 提供了一个compiler方法,用于编译 Solidity 代码。该方法可以根据源代码是否有任何导入来以两种不同的方式使用。如果源代码没有任何导入,则它需要两个参数;即,第一个参数是 Solidity 源代码作为字符串,第二个参数是一个布尔值,表示是否优化字节码。如果源字符串包含多个合约,则它会将它们全部编译。

这里有一个示例来演示这一点:

var solc = require("solc"); 
var input = "contract x { function g() {} }"; 
var output = solc.compile(input, 1); // 1 activates the optimiser  
for (var contractName in output.contracts) { 
    // logging code and ABI  
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 
    console.log(contractName + "; " + JSON.parse(output.contracts[contractName].interface)); 
} 

如果您的源代码包含导入,则第一个参数将是一个对象,其键是文件名,值是文件的内容。因此,每当编译器看到一个导入语句时,它不会在文件系统中查找文件;相反,它会通过与键匹配的文件名在对象中查找文件内容。以下是一个演示示例:

var solc = require("solc"); 
var input = { 
    "lib.sol": "library L { function f() returns (uint) { return 7; } }", 
    "cont.sol": "import 'lib.sol'; contract x { function g() { L.f(); } }" 
}; 
var output = solc.compile({sources: input}, 1); 
for (var contractName in output.contracts) 
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 

如果您想在编译过程中从文件系统读取导入的文件内容,或者解析编译过程中的文件内容,则编译器方法支持第三个参数,该参数是一个方法,接受文件名并应返回文件内容。以下是一个演示示例:

var solc = require("solc"); 
var input = { 
    "cont.sol": "import 'lib.sol'; contract x { function g() { L.f(); } }" 
}; 
function findImports(path) { 
    if (path === "lib.sol") 
        return { contents: "library L { function f() returns (uint) { return 7; } }" } 
    else 
        return { error: "File not found" } 
} 
var output = solc.compile({sources: input}, 1, findImports); 
for (var contractName in output.contracts) 
    console.log(contractName + ": " + output.contracts[contractName].bytecode); 

使用不同的编译器版本

要使用不同版本的 Solidity 编译合约,您需要使用useVersion方法获取不同编译器的引用。useVersion接受一个字符串,指示保存编译器的 JavaScript 文件名,并在/node_modules/solc/bin目录中查找该文件。

solcjs 还提供了另一个方法称为loadRemoteVersion,它接受与solc-bin存储库的solc-bin/bin目录中的文件名匹配的编译器文件名,并下载并使用它。

最后,solcjs 还提供了另一个方法称为setupMethods,它类似于useVersion但可以从任何目录加载编译器。

这里有一个示例来演示所有三种方法:

var solc = require("solc"); 

var solcV047 = solc.useVersion("v0.4.7.commit.822622cf"); 
var output = solcV011.compile("contract t { function g() {} }", 1); 

solc.loadRemoteVersion('soljson-v0.4.5.commit.b318366e', function(err, solcV045) { 
    if (err) { 
        // An error was encountered, display and quit 
    } 

    var output = solcV045.compile("contract t { function g() {} }", 1); 
}); 

var solcV048 = solc.setupMethods(require("/my/local/0.4.8.js")); 
var output = solcV048.compile("contract t { function g() {} }", 1); 

solc.loadRemoteVersion('latest', function(err, latestVersion) { 
    if (err) { 
        // An error was encountered, display and quit 
    } 
    var output = latestVersion.compile("contract t { function g() {} }", 1); 
}); 

要运行上述代码,您首先需要从solc-bin存储库下载v0.4.7.commit.822622cf.js文件,并将其放置在node_modules/solc/bin目录中。然后,您需要下载 Solidity 版本 0.4.8 的编译器文件,并将其放置在文件系统的某个位置,并将路径指向setupMethods调用中的该目录。

链接库

如果你的 Solidity 源代码引用了库,那么生成的字节码将包含引用库的真实地址的占位符。这些必须通过一个称为链接的过程来更新,然后再部署合约。

solcjs 提供了linkByteCode方法来将库地址链接到生成的字节码上。

这里是一个示例来演示这一点:

var solc = require("solc"); 

var input = { 
    "lib.sol": "library L { function f() returns (uint) { return 7; } }", 
    "cont.sol": "import 'lib.sol'; contract x { function g() { L.f(); } }" 
}; 

var output = solc.compile({sources: input}, 1); 

var finalByteCode = solc.linkBytecode(output.contracts["x"].bytecode, { 'L': '0x123456...' }); 

更新 ABI

合约的 ABI 提供了除实现以外的各种关于合约的信息。由于高版本支持更多的 Solidity 特性,因此两个不同版本的编译器生成的 ABI 可能不匹配;因此,它们会在 ABI 中包含额外的内容。例如,回退函数是在 Solidity 0.4.0 版本中引入的,因此使用版本低于 0.4.0 的编译器生成的 ABI 将不包含有关回退函数的信息,这些智能合约的行为就像它们有一个空主体和一个可支付修饰符的回退函数一样。因此,API 应该进行更新,以便依赖于较新 Solidity 版本的 ABI 的应用程序能够获得更好的合约信息。

solcjs 提供了一个 API 来更新 ABI。下面是一个示例代码来演示这一点:

var abi = require("solc/abi"); 

var inputABI = [{"constant":false,"inputs":[],"name":"hello","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"}]; 
var outputABI = abi.update("0.3.6", inputABI) 

这里,0.3.6 表示 ABI 是使用 0.3.6 版本的编译器生成的。由于我们使用的是 solcjs 版本 0.4.8,因此 ABI 将被更新以匹配由 0.4.8 编译器版本生成的 ABI,而不是更新到更高版本。

上述代码的输出将如下所示:

[{"constant":false,"inputs":[],"name":"hello","outputs":[{"name":"","type":"string"}],"payable":true,"type":"function"},{"type":"fallback","payable":true}] 

构建合约部署平台

现在我们已经学会了如何使用 solcjs 编译 Solidity 源代码,是时候建立一个平台,让我们能够编写、编译和部署合约了。我们的平台将允许用户提供他们的账户地址和私钥,使用这些信息,我们的平台将部署合约。

在开始构建应用程序之前,请确保你正在运行 geth 开发实例,该实例正在挖矿,启用了rpc,并通过 HTTP-RPC 服务器公开了ethweb3txpool API。你可以通过运行以下命令来完成所有这些操作:

    geth --dev --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545" --mine --rpcapi "eth,txpool,web3"

项目结构

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

要测试Final目录,你需要在其中运行npm install,然后在Final目录内使用node app.js命令运行应用程序。

Initial目录中,你会找到一个public目录和两个文件,分别命名为app.jspackage.jsonpackage.json文件包含了我们应用程序的后端依赖关系。app.js是你将放置后端源代码的地方。

public 目录包含与前端相关的文件。在 public/css 中,你会找到 bootstrap.min.css,这是 bootstrap 库,你还会找到 codemirror.css 文件,这是 codemirror 库的 CSS。在 public/html 中,你会找到 index.html,你会在这里放置我们应用的 HTML 代码,在 public/js 目录中,你会找到 codemirror 和 web3.js 的 .js 文件。在 public/js 中,你还会找到一个 main.js 文件,你会在这里放置我们应用的前端 JS 代码。

构建后端

让我们首先构建应用的后端。首先,在 Initial 目录中运行 npm install 安装我们后端所需的依赖项。

这是运行 express 服务并提供 index.html 文件和静态文件的后端代码:

var express = require("express");   
var app = express();   

app.use(express.static("public")); 

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

app.listen(8080); 

前述代码不言自明。现在让我们进一步进行。我们的应用将有两个按钮,即编译和部署。当用户单击编译按钮时,合约将被编译,当用户单击部署按钮时,合约将被部署。

我们将在后端进行编译和部署合约。虽然这可以在前端完成,但我们将在后端完成,因为 solcjs 仅适用于 Node.js(尽管它使用的基于 JavaScript 的编译器可以在前端工作)。

要了解如何在前端编译,请查看 solcjs 的源代码,这将让你了解 JavaScript-based 编译器暴露的 API。

当用户单击编译按钮时,前端将通过传递合约源代码向 /compile 路径发出 GET 请求。以下是路由的代码:

var solc = require("solc"); 

app.get("/compile", function(req, res){ 
 var output = solc.compile(req.query.code, 1); 
 res.send(output); 
}) 

首先,我们在这里导入 solcjs 库。然后,我们定义 /compile 路由,在路由回调中,我们只是编译客户端发送的源代码,并启用优化器。然后,我们只需将 solc.compile 方法的返回值发送到前端,让客户端检查编译是否成功。

当用户单击部署按钮时,前端将通过传递合约源代码和构造函数参数从地址和私钥向 /deploy 路径发出 GET 请求。当用户单击此按钮时,将部署合约并将事务哈希返回给用户。

这是此代码的代码:

var Web3 = require("web3"); 
var BigNumber = require("bignumber.js"); 
var ethereumjsUtil = require("ethereumjs-util"); 
var ethereumjsTx = require("ethereumjs-tx"); 

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

function etherSpentInPendingTransactions(address, callback) 
{ 
 web3.currentProvider.sendAsync({ 
    method: "txpool_content", 
    params: [], 
    jsonrpc: "2.0", 
    id: new Date().getTime() 
 }, function (error, result) { 
  if(result.result.pending) 
  { 
   if(result.result.pending[address]) 
   { 
    var txns = result.result.pending[address]; 
    var cost = new BigNumber(0); 

    for(var txn in txns) 
    { 
     cost = cost.add((new BigNumber(parseInt(txns[txn].value))).add((new BigNumber(parseInt(txns[txn].gas))).mul(new BigNumber(parseInt(txns[txn].gasPrice))))); 
    } 

    callback(null, web3.fromWei(cost, "ether")); 
   } 
   else 
   { 
    callback(null, "0"); 
   } 
  } 
  else 
  { 
   callback(null, "0"); 
  } 
 }) 
} 

function getNonce(address, callback) 
{ 
 web3.eth.getTransactionCount(address, function(error, result){ 
  var txnsCount = result; 

  web3.currentProvider.sendAsync({ 
     method: "txpool_content", 
     params: [], 
     jsonrpc: "2.0", 
     id: new Date().getTime() 
  }, function (error, result) { 
   if(result.result.pending) 
   { 
    if(result.result.pending[address]) 
    { 
     txnsCount = txnsCount + Object.keys(result.result.pending[address]).length; 
     callback(null, txnsCount); 
    } 
    else 
    { 
     callback(null, txnsCount); 
    } 
   } 
   else 
   { 
    callback(null, txnsCount); 
   } 
  }) 
 }) 
} 

app.get("/deploy", function(req, res){ 
 var code = req.query.code; 
 var arguments = JSON.parse(req.query.arguments); 
 var address = req.query.address; 

 var output = solc.compile(code, 1); 

 var contracts = output.contracts; 

 for(var contractName in contracts) 
 { 
  var abi = JSON.parse(contracts[contractName].interface); 
  var byteCode = contracts[contractName].bytecode; 

  var contract = web3.eth.contract(abi); 

  var data = contract.new.getData.call(null, ...arguments, { 
   data: byteCode 
  }); 

  var gasRequired = web3.eth.estimateGas({ 
      data: "0x" + data 
  }); 

  web3.eth.getBalance(address, function(error, balance){ 
   var etherAvailable = web3.fromWei(balance, "ether"); 
   etherSpentInPendingTransactions(address, function(error, balance){ 
    etherAvailable = etherAvailable.sub(balance) 
    if(etherAvailable.gte(web3.fromWei(new BigNumber(web3.eth.gasPrice).mul(gasRequired), "ether"))) 
    { 
     getNonce(address, function(error, nonce){ 
      var rawTx = { 
             gasPrice: web3.toHex(web3.eth.gasPrice), 
             gasLimit: web3.toHex(gasRequired), 
             from: address, 
             nonce: web3.toHex(nonce), 
             data: "0x" + data 
         }; 

      var privateKey = ethereumjsUtil.toBuffer(req.query.key, 'hex'); 
      var tx = new ethereumjsTx(rawTx); 
      tx.sign(privateKey); 

      web3.eth.sendRawTransaction("0x" + tx.serialize().toString('hex'), function(err, hash) { 
       res.send({result: { 
        hash: hash, 
       }}); 
      }); 
     }) 
    } 
    else 
    { 
     res.send({error: "Insufficient Balance"}); 
    } 
   }) 
  }) 

     break; 
 } 
}) 

这是前述代码的工作方式:

  1. 首先,Web 导入 web3.jsBigNumber.jsethereumjs-utilethereumjs-tx 库。然后,我们创建了 Web3 的实例。

  2. 然后,我们定义一个名为etherInSpentPendingTransactions的函数,它计算地址的待处理交易中所花费的总以太币。由于web3.js不提供与事务池相关的 JavaScript API,我们使用web3.currentProvider.sendAsync来做一个原始的 JSON-RPC 调用。sendAsync用于异步进行原始的 JSON-RPC 调用。如果你想以同步的方式进行此调用,那么请使用send方法而不是sendAsync。在计算地址的待处理事务中的总以太币时,我们寻找事务池中的待处理事务而不是待处理区块,因为前面我们讨论的问题。在计算总以太币时,我们将每个交易的值和 gas 添加,因为 gas 也会减少以太币余额。

  3. 接下来,我们定义一个名为getNonce的函数,使用我们之前讨论的技术来检索地址的 nonce。它只是将已挖掘交易的总数加上待处理交易的总数。

  4. 最后,我们声明了/deploy端点。首先,我们编译合同。然后,我们只部署第一个合同。如果在提供的源代码中发现了多个合同,我们的平台设计为只部署第一个合同。你可以稍后增强应用程序来部署所有已编译的合同,而不仅仅是第一个合同。然后,我们使用web3.eth.contract创建一个合同对象。

  5. 由于我们没有使用 hooked-web3-provider 或任何拦截 sendTransactions 并将其转换为 sendRawTransaction 调用的方法,在部署合同时,我们现在需要生成交易数据部分,其中将合同字节码和构造函数参数组合并编码为十六进制字符串。实际上,合同对象让我们生成交易的数据。这可以通过调用具有函数参数的 getData 方法来完成。如果你想要获取部署合同的数据,然后调用 contract.new.getData,如果你想调用合同的一个函数,那么调用 contract.functionName.getData。在这两种情况下,提供参数给 getData 方法。因此,要生成交易的数据,你只需要合同的 ABI。要了解如何组合和编码函数名称和参数以生成数据,你可以查看github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#examples,但如果你有合同的 ABI 或知道如何手动创建 ABI,则这将不需要。

  6. 然后,我们使用web3.eth.estimateGas来计算部署合同所需的 gas 量。

  7. 稍后,我们检查地址是否有足够的以太来支付部署合约所需的 gas。我们通过检索地址的余额并将其减去在未决交易中花费的余额来找到这一点,然后检查剩余余额是否大于或等于所需 gas 的以太数量。

  8. 最后,我们获取 nonce,签名并广播交易。我们简单地将交易哈希返回给前端。

构建前端

现在让我们构建应用程序的前端。我们的前端将包含一个编辑器,用户使用该编辑器编写代码。当用户单击编译按钮时,我们将动态显示输入框,每个输入框将表示一个构造函数参数。当单击部署按钮时,将从这些输入框中获取构造函数参数值。用户将需要在这些输入框中输入 JSON 字符串。

我们将使用 codemirror 库来集成编辑器到我们的前端。要了解更多关于如何使用 codemirror 的信息,请参考codemirror.net/

这是我们应用程序的前端 HTML 代码。将此代码放入index.html文件中:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="utf-8"> 
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
        <meta http-equiv="x-ua-compatible" content="ie=edge"> 
        <link rel="stylesheet" href="/css/bootstrap.min.css"> 
        <link rel="stylesheet" href="/css/codemirror.css"> 
        <style type="text/css"> 
            .CodeMirror 
            { 
                height: auto; 
            } 
        </style> 
    </head> 
    <body> 
        <div class="container"> 
            <div class="row"> 
                <div class="col-md-6"> 
                    <br> 
                    <textarea id="editor"></textarea> 
                    <br> 
                    <span id="errors"></span> 
                    <button type="button" id="compile" class="btn btn-primary">Compile</button> 
                </div> 
                <div class="col-md-6"> 
                    <br> 
                    <form> 
                        <div class="form-group"> 
                            <label for="address">Address</label> 
                            <input type="text" class="form-control" id="address" placeholder="Prefixed with 0x"> 
                        </div> 
                        <div class="form-group"> 
                            <label for="key">Private Key</label> 
                            <input type="text" class="form-control" id="key" placeholder="Prefixed with 0x"> 
                        </div> 
                        <hr> 
                        <div id="arguments"></div> 
                        <hr> 
                        <button type="button" id="deploy" class="btn btn-primary">Deploy</button> 
                    </form> 
                </div> 
            </div> 
        </div> 
        <script src="img/codemirror.js"></script> 
        <script src="img/main.js"></script> 
    </body> 
</html> 

在这里,您可以看到我们有一个textareatextarea标签将保存用户在 codemirror 编辑器中输入的内容。前面代码中的其他内容都是不言自明的。

这是完整的前端 JavaScript 代码。将此代码放入main.js文件中:

var editor = CodeMirror.fromTextArea(document.getElementById("editor"), { 
    lineNumbers: true, 
}); 

var argumentsCount = 0; 

document.getElementById("compile").addEventListener("click", function(){ 
 editor.save(); 
 var xhttp = new XMLHttpRequest(); 

 xhttp.onreadystatechange = function() { 
     if (this.readyState == 4 && this.status == 200) { 
      if(JSON.parse(xhttp.responseText).errors != undefined) 
      { 
       document.getElementById("errors").innerHTML = JSON.parse(xhttp.responseText).errors + "<br><br>"; 
      } 
      else 
      { 
       document.getElementById("errors").innerHTML = ""; 
      } 

      var contracts = JSON.parse(xhttp.responseText).contracts; 

      for(var contractName in contracts) 
      { 
       var abi = JSON.parse(contracts[contractName].interface); 

       document.getElementById("arguments").innerHTML = ""; 

       for(var count1 = 0; count1 < abi.length; count1++) 
       { 
        if(abi[count1].type == "constructor") 
        { 
         argumentsCount = abi[count1].inputs.length; 

         document.getElementById("arguments").innerHTML = '<label>Arguments</label>'; 

         for(var count2 = 0; count2 < abi[count1].inputs.length; count2++) 
         { 
          var inputElement = document.createElement("input"); 
          inputElement.setAttribute("type", "text"); 
          inputElement.setAttribute("class", "form-control"); 
          inputElement.setAttribute("placeholder", abi[count1].inputs[count2].type); 
          inputElement.setAttribute("id", "arguments-" + (count2 + 1)); 

          var br = document.createElement("br"); 

          document.getElementById("arguments").appendChild(br); 
          document.getElementById("arguments").appendChild(inputElement); 
         } 

         break; 
        } 
       } 

       break; 
      } 
     } 
 }; 

 xhttp.open("GET", "/compile?code=" + encodeURIComponent(document.getElementById("editor").value), true); 
 xhttp.send();  
}) 

document.getElementById("deploy").addEventListener("click", function(){ 
 editor.save(); 

 var arguments = []; 

 for(var count = 1; count <= argumentsCount; count++) 
 { 
  arguments[count - 1] = JSON.parse(document.getElementById("arguments-" + count).value);  
 } 

 var xhttp = new XMLHttpRequest(); 

 xhttp.onreadystatechange = function() { 
     if (this.readyState == 4 && this.status == 200)  
     { 
      var res = JSON.parse(xhttp.responseText); 

      if(res.error) 
      { 
       alert("Error: " + res.error) 
      } 
      else 
      { 
       alert("Txn Hash: " + res.result.hash); 
      }  
     } 
     else if(this.readyState == 4) 
     { 
      alert("An error occured."); 
     } 
 }; 

 xhttp.open("GET", "/deploy?code=" + encodeURIComponent(document.getElementById("editor").value) + "&arguments=" + encodeURIComponent(JSON.stringify(arguments)) + "&address=" + document.getElementById("address").value + "&key=" + document.getElementById("key").value, true); 
 xhttp.send();  
}) 

以下是前面代码的工作方式:

  1. 首先,我们将代码编辑器添加到网页中。代码编辑器将显示在textarea的位置,而textarea将被隐藏。

  2. 然后我们有编译按钮的点击事件处理程序。在其中,我们保存编辑器,将编辑器的内容复制到textarea中。当单击编译按钮时,我们向/compile路径发出请求,一旦获得结果,我们解析它并显示输入框,以便用户可以输入构造函数参数。在这里,我们只读取第一个合约的构造函数参数。但是,如果有多个合约,则可以增强 UI 以显示所有合约的构造函数的输入框。

  3. 最后,我们有部署按钮的点击事件处理程序。在这里,我们读取构造函数参数的值,解析并将它们放入一个数组中。然后,我们通过传递地址、密钥、代码和参数值向/deploy端点添加一个请求。如果有错误,则我们在弹出窗口中显示错误;否则,我们在弹出窗口中显示交易哈希。

测试

要测试应用程序,请在Initial目录内运行app.js节点,并访问localhost:8080。您将看到以下截图中显示的内容:

现在输入一些 Solidity 合约代码并按编译按钮。然后,您将能够看到新的输入框出现在右侧。例如,看一下以下截图:

现在输入一个有效地址及其关联的私钥。然后输入构造函数参数的值并点击部署。如果一切正常,你将看到一个带有交易哈希的警报框。例如,看一下下面的屏幕截图:

总结

在本章中,我们学习了如何使用交易池 API,如何计算正确的 nonce,计算可支出的余额,生成交易的数据,编译合约等等。然后我们构建了一个完整的合同编译和部署平台。现在你可以继续增强我们构建的应用程序,部署编辑器中找到的所有合同,处理导入,添加库等等。

在下一章中,我们将通过构建一个去中心化的赌博应用程序来学习 Oraclize。

第七章:构建一个投注应用

有时,智能合约需要访问其他 DApp 或来自万维网的数据是必要的。但是由于技术和共识方面的挑战,让智能合约访问外部数据确实非常复杂。因此,目前,以太坊智能合约没有原生支持访问外部数据。但是有第三方解决方案供以太坊智能合约访问一些流行的 DApp 和来自万维网的数据。在本章中,我们将学习如何使用 Oraclize 从以太坊智能合约中发出 HTTP 请求,以访问来自万维网的数据。我们还将学习如何访问存储在 IPFS 中的文件,使用 strings 库来处理字符串等等。我们将通过构建一个足球投注智能合约和其客户端来学习所有这些。

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

  • Oraclize 是如何工作的?

  • Oraclize 有哪些不同的数据源,它们每一个是如何工作的?

  • Oraclize 中的共识是如何工作的?

  • 将 Oraclize 集成到以太坊智能合约中

  • 使用 Solidity 库中的字符串库使字符串处理变得更加简单

  • 构建一个足球投注应用

介绍 Oraclize

Oraclize 是一个旨在使智能合约能够从其他区块链和万维网获取数据的服务。该服务目前在比特币和以太坊的测试网和主网上运行。Oraclize 的特殊之处在于,你无需信任它,因为它提供给智能合约的所有数据都有真实性证明。

在本章中,我们的目标是学习以太坊智能合约如何使用 Oraclize 服务从万维网获取数据。

它是如何工作的?

让我们看看以太坊智能合约如何使用 Oraclize 从其他区块链和万维网获取数据的过程。

要获取存在于以太坊区块链之外的数据,以太坊智能合约需要向 Oraclize 发送查询,提及数据源(表示从何处获取数据)和数据源的输入(表示要获取的内容)。

发送查询到 Oraclize 意味着向存在于以太坊区块链中的 Oraclize 合约发送合约调用(即,内部交易)。

Oraclize 服务器不断查找其智能合约的新进入查询。每当它看到一个新的查询时,它就会获取结果并通过调用您合约的 _callback 方法将其发送回您的合约。

数据源

以下是 Oraclize 允许智能合约获取数据的源列表:

  • URL: URL 数据源使您能够进行 HTTP GET 或 POST 请求,即从万维网获取数据。

  • WolframAlpha: WolframAlpha 数据源使您能够向 WolframAlpha 知识引擎提交查询并获得答案。

  • Blockchainblockchain 数据源提供了从其他区块链访问数据的能力。可以提交给blockchain数据源的可能查询包括bitcoin blockchain heightlitecoin hashratebitcoin difficulty1NPFRDJuEdyqEn2nmLNaWMfojNksFjbL4S balance等。

  • IPFSIPFS 数据源提供了获取存储在IPFS中文件内容的能力。

  • Nestednested 数据源是一个元数据源;它不提供对额外服务的访问。它的设计是为了提供一种简单的聚合逻辑,使单个查询能够利用基于任何可用数据源的子查询,并产生单个字符串作为结果;例如:

[WolframAlpha] ${[IPFS] QmP2ZkdsJG7LTw7jBbizTTgY1ZBeen64PqMgCAWz2koJBL}中的温度。

  • Computationcomputation 数据源使给定应用程序在安全的链下环境中可审计地执行;也就是说,它让我们获取应用程序链下执行的结果。这个应用程序必须在退出前在最后一行(标准输出上)打印查询结果。执行环境必须由 Dockerfile 描述,其中构建和运行应用程序应立即启动您的主应用程序。Dockerfile 初始化加上您的应用程序执行应尽快终止:在AWS t2.micro实例上的最大执行超时为 5 分钟。在这里,我们考虑的是AWS t2.micro实例,因为这是 Oraclize 将用来执行该应用程序的实例。由于数据源的输入是包含这些文件的 ZIP 存档的 IPFS 多哈希值(Dockerfile 加上任何外部文件依赖项,且 Dockerfile 必须放在存档根目录中),您应该注意准备这个存档并预先将其推送到 IPFS。

这些数据源在编写本书时可用。但未来可能会有更多的数据源可用。

真实性证明

虽然 Oraclize 是一个值得信赖的服务,但你可能仍然想要检查 Oraclize 返回的数据是否真实,即它是否在传输过程中被 Oraclize 或其他人篡改。

可选地,Oraclize 提供了从 URL、区块链、嵌套和计算数据源返回的 TLSNotary 结果证明。对于WolframAlphaIPFS数据源,这种证明是不可用的。目前,Oraclize 仅支持 TLSNotary 证明,但在将来,他们可能会支持其他一些身份验证方式。目前,TLSNotary 证明需要手动验证,但 Oraclize 已经在进行链上证明验证;也就是说,你的智能合约代码可以在从 Oraclize 接收数据时自行验证 TLSNotary 证明,以便在证明无效时丢弃这些数据。

这个工具(github.com/Oraclize/proof-verification-tool)是由 Oraclize 提供的开源工具,用于验证 TLSNotary 证明,如果你想要的话。

理解 TLSNotary 如何工作并不是使用 Oraclize 或验证证明所必需的。验证 TLSNotary 证明的工具是开源的;因此,如果它包含任何恶意代码,那么它很容易被发现,因此这个工具是可信的。

让我们来看看 TLSNotary 如何工作的概述。要理解 TLSNotary 的工作原理,首先需要了解 TLS 的工作原理。TLS 协议提供了一种方法,让客户端和服务器创建一个加密会话,这样其他人就无法阅读或篡改客户端和服务器之间传输的内容。服务器首先将其证书(由受信任的 CA 颁发给域所有者)发送给客户端。证书将包含服务器的公钥。客户端使用 CA 的公钥解密证书,以便可以验证证书实际上是由 CA 颁发的,并获取服务器的公钥。然后,客户端生成一个对称加密密钥和一个 MAC 密钥,并使用服务器的公钥对它们进行加密,然后将其发送给服务器。服务器只能解密此消息,因为它有解密它的私钥。现在客户端和服务器共享相同的对称和 MAC 密钥,除了它们以外,没有其他人知道这些密钥,它们可以开始相互发送和接收数据。对称密钥用于加密和解密数据,其中 MAC 密钥和对称密钥一起用于为加密消息生成签名,以便在消息被攻击者修改时,另一方可以知道它。

TLSNotary 是 TLS 的修改版,Oraclize 使用它来提供密码学证明,证明它们提供给您的智能合约的数据确实是数据源在特定时间提供给 Oraclize 的数据。实际上,TLSNotary 协议是 PageSigner 项目开发和使用的开源技术。

TLSNotary 的工作原理是将对称密钥和 MAC 密钥分配给三个参与方,即服务器、被审计者和审计员。TLSNotary 的基本思想是被审计者可以向审计员证明特定结果是服务器在特定时间返回的。

下面是 TLSNotary 如何实现这一点的概述。审计员计算对称密钥和 MAC 密钥,然后仅将对称密钥提供给受审者。由于 MAC 签名检查确保了服务器传输的 TLS 数据未被修改,因此受审者不需要 MAC 密钥。有了对称加密密钥,受审者现在可以解密服务器的数据。因为所有消息都是由银行使用 MAC 密钥“签名”的,而且只有服务器和审计员知道 MAC 密钥,正确的 MAC 签名可以作为证明某些消息来自银行且未被受审者伪造的证据。

在 Oraclize 服务的情况下,Oraclize 是受审者,而一个特制的、开源的 Amazon 机器映像的受限 AWS 实例充当审计员。

他们提供的证据数据是此 AWS 实例的签名证明,证明了一个正确的 TLSnotary 证明确实发生了。他们还提供了一些关于在 AWS 实例中运行的软件的额外证据,即它是否自初始化以来已被修改。

定价

任何以太坊地址发起的第一个 Oraclize 查询调用完全免费。在测试网络上使用 Oraclize 调用是免费的!这仅适用于测试环境中的适度使用。

从第二次调用开始,你必须支付以太币来进行查询。当向 Oraclize 发送查询(即进行内部交易调用)时,通过将以太币从调用合约转移到 Oraclize 合约来扣除费用。要扣除的以太币数量取决于数据源和证明类型。

下表显示了发送查询时扣除的以太币数量:

数据源 无证明 使用 TLSNotary 证明
URL $0.01 | $0.05
区块链 $0.01 | $0.05
WolframAlpha $0.03 | $0.03
IPFS $0.01 | $0.01

如果你正在发起一个 HTTP 请求,并且想要 TLSNotary 证明,那么调用的合约必须有价值$0.05 的以太币;否则,将抛出异常。

使用 Oraclize API 入门

要使合约使用 Oraclize 服务,需要继承usingOraclize合约。你可以在github.com/oraclize/ethereum-api找到这个合约。

usingOraclize合约充当OraclizeIOraclizeAddrResolverI合约的代理。实际上,usingOraclize使调用OraclizeIOraclizeAddrResolverI合约变得容易,也就是说,它提供了更简单的 API。如果你感觉自如,你也可以直接调用OraclizeIOraclizeAddrResolverI合约。你可以查看这些合约的源代码,找到所有可用的 API。我们只会学习最必要的 API。

让我们看看如何设置证明类型、设置证明存储位置、进行查询、找到查询的成本等等。

设置证明类型和存储位置

无论你是否需要来自 Oraclize 的 TLSNotary 证明,你都必须在发出查询之前指定证明类型和证明存储位置。

如果你不需要证明,那么将这段代码放入你的合约中:

oraclize_setProof(proofType_NONE)

如果你需要证明,那么将这段代码放入你的合约中:

oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS)

目前,proofStorage_IPFS是唯一可用的证明存储位置;也就是说,TLSNotary 证明只存储在IPFS中。

你可以仅执行这些方法之一,例如,在构造函数中或在其他任何时候,例如,如果你只需要某些查询的证明。

发送查询

要向 Oraclize 发送查询,你需要调用oraclize_query函数。该函数至少需要两个参数,即数据源和给定数据源的输入。数据源参数不区分大小写。

下面是oraclize_query函数的一些基本示例:

oraclize_query("WolframAlpha", "random number between 0 and 100"); 

oraclize_query("URL", "https://api.kraken.com/0/public/Ticker?pair=ETHXBT"); 

oraclize_query("IPFS", "QmdEJwJG1T9rzHvBD8i69HHuJaRgXRKEQCP7Bh1BVttZbU"); 

oraclize_query("URL", "https://xyz.io/makePayment", '{"currency": "USD", "amount": "1"}');

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

  • 如果第一个参数是一个字符串,则假定它是数据源,第二个参数则假定是数据源的输入。在第一次调用中,数据源是WolframAlpha,我们发送给它的搜索查询是random number between 0 and 100

  • 在第二次调用中,我们向第二个参数中的URL发出一个HTTP GET请求。

  • 在第三次调用中,我们从IPFS中获取QmdEJwJG1T9rzHvBD8i69HHuJaRgXRKEQCP7Bh1BVttZbU文件的内容。

  • 如果数据源之后的两个连续参数都是字符串,则假定它是一个 POST 请求。在最后一次调用中,我们向https://xyz.io/makePayment发出一个HTTP POST请求,而POST请求主体内容是第三个参数中的字符串。Oraclize 能够根据字符串格式检测到内容类型头部。

定时查询

如果你希望 Oraclize 在将来的某个预定时间执行你的查询,只需将延迟时间(以秒为单位)从当前时间作为第一个参数指定。

这是一个例子:

oraclize_query(60, "WolframAlpha", "random number between 0 and 100");

上述查询将在被看到后的 60 秒内由 Oraclize 执行。因此,如果第一个参数是一个数字,则假定我们正在安排一个查询。

自定义燃气

从 Oraclize 到你的__callback函数的交易需要燃气,就像任何其他交易一样。你需要支付给 Oraclize 燃气成本。oraclize_query 收取用于发出查询的以太币,同时也用于在调用__callback函数时提供燃气。默认情况下,Oraclize 在调用__callback函数时提供 200,000 gas。

返回的燃气成本实际上是由你控制的,因为你在__callback方法中编写代码,因此可以估算出来。因此,当向 Oraclize 发出查询时,你还可以指定__callback交易的gasLimit应该是多少。然而,请注意,由于 Oraclize 发送交易,任何未使用的 gas 都会退还给 Oraclize,而不是你。

如果默认值和最小值的 200,000 gas 不够用,你可以通过以下方式增加它:

oraclize_query("WolframAlpha", "random number between 0 and 100", 500000);

在这里,你可以看到如果最后一个参数是一个数字,那么就假设它是自定义 gas。在前面的代码中,Oraclize 将为回调交易使用 500k 的 gasLimit 而不是 200k。因为我们要求 Oraclize 提供更多的 gas,所以在调用 oraclize_query 时,Oraclize 将扣除更多的以太币(取决于需要多少 gas)。

请注意,如果你提供的 gasLimit 太低,并且你的 __callback 方法很长,你可能永远不会看到回调。另请注意,自定义 gas 必须大于 200k。

回调函数

一旦你的结果准备好了,Oraclize 将会发送一个交易回到你的合约地址并调用以下三种方法之一:

  • 或者 __callback(bytes32 myid, string result)Myid 是每个查询的唯一 ID。这个 ID 是由 oraclize_query 方法返回的。如果你的合约中有多个 oraclize_query 调用,那么这个 ID 就用于匹配此结果所属的查询。

  • 如果你请求了 TLS Notary 证明,这就是结果:__callback(bytes32 myid, string result, bytes proof)

  • 作为最后的手段,如果其他方法都不存在,回退函数是 function()

这是 __callback 函数的一个例子:

function __callback(bytes32 myid, string result) { 
    if (msg.sender != oraclize_cbAddress()) throw; // just to be sure the calling address is the Oraclize authorized one 

    //now doing something with the result.. 
}

解析辅助函数

从 HTTP 请求返回的结果可以是 HTML、JSON、XML、二进制等。在 Solidity 中,解析结果是困难且昂贵的。因此,Oraclize 提供了解析辅助函数,让它在其服务器上处理解析,并且你只获取你需要的结果的一部分。

要求 Oraclize 解析结果,你需要使用以下解析辅助函数包装 URL:

  • xml(..)json(..) 辅助函数让你可以要求 Oraclize 只返回 JSON 或 XML 解析后的响应的一部分;例如,看一下以下示例:

    • 为了获取整个响应,你可以使用 URL 数据源和 api.kraken.com/0/public/Ticker?pair=ETHUSD URL 参数

    • 如果你只想获取最后价格字段,你需要使用 JSON 解析调用,如 json(api.kraken.com/0/public/Ticker?pair=ETHUSD).result.XETHZUSD.c.0

  • html(..).xpath(..) 辅助函数对于 HTML 抓取非常有用。只需将你想要的 XPATH 指定为 xpath(..) 参数;例如,看一下以下示例:

    • 要获取特定推文的文本,使用 html(https://twitter.com/oraclizeit/status/671316655893561344).xpath(//*[contains(@class, 'tweet-text')]/text())
  • binary(..) 辅助函数对于获取二进制文件(例如证书文件)非常有用:

    • 要仅获取二进制文件的一部分,你可以使用 slice(offset,length);第一个参数是偏移量,而第二个参数是你希望返回的切片的长度(都以字节为单位)。

    • 示例:仅从二进制 CRL 中获取前 300 个字节,binary(https://www.sk.ee/crls/esteid/esteid2015.crl).slice(0,300)。二进制辅助程序必须与切片选项一起使用,只接受二进制文件(未编码)。

如果服务器无响应或无法访问,我们将发送空响应。您可以使用app.Oraclize.it/home/test_query测试查询。

获取查询价格

如果您想在实际查询之前知道查询的费用,那么您可以使用Oraclize.getPrice()函数来获取所需的 wei 数量。它接受的第一个参数是数据源,第二个参数是可选的,即自定义的 gas。

这个的一个流行用例是通知客户如果合同中没有足够的以太币来进行查询,则添加以太币。

加密查询

有时,您可能不希望揭示数据源和/或数据源的输入。例如:如果有的话,您可能不希望在 URL 中揭示 API 密钥。因此,Oraclize 提供了一种将查询加密存储在智能合约中,并且只有 Oraclize 的服务器有解密密钥的方法。

Oraclize 提供了一个 Python 工具(github.com/Oraclize/encrypted-queries),可用于加密数据源和/或数据输入。它生成一个非确定性的加密字符串。

加密任意文本字符串的 CLI 命令如下:

    python encrypted_queries_tools.py -e -p 044992e9473b7d90ca54d2886c7addd14a61109af202f1c95e218b0c99eb060c7134c4ae46345d0383ac996185762f04997d6fd6c393c86e4325c469741e64eca9 "YOUR DATASOURCE or INPUT"

您看到的长十六进制字符串是 Oraclize 服务器的公钥。现在您可以使用前述命令的输出来替代数据源和/或数据源的输入。

为了防止加密查询的滥用(即重放攻击),使用特定加密查询的第一个与 Oraclize 查询的合同将成为其合法所有者。任何其他重用完全相同字符串的合同将不被允许使用它,并将收到空结果。因此,请记住在重新部署使用加密查询的合同时始终生成新的加密字符串。

解密数据源

还有另一个名为解密的数据源。它用于解密加密的字符串。但是这个数据源不返回任何结果;否则,任何人都将有能力解密数据源和数据源的输入。

它专门设计用于在嵌套数据源内部使用,以实现部分查询加密。这是它唯一的用例。

Oraclize Web IDE

Oraclize 提供了一个 Web IDE,您可以在其中编写、编译和测试基于 Oraclize 的应用程序。您可以在dapps.Oraclize.it/browser-Solidity/找到它。

如果您访问链接,那么您会注意到它看起来与浏览器 Solidity 完全相同。实际上,它就是带有一个额外功能的浏览器 Solidity。要理解这个功能是什么,我们需要更深入地了解浏览器 Solidity。

浏览器 Solidity 不仅让我们编写、编译和为我们的合约生成 web3.js 代码,还可以在那里测试这些合约。直到现在,为了测试我们的合约,我们都是设置了以太坊节点并向其发送交易。但是浏览器 Solidity 可以在不连接到任何节点的情况下执行合约,所有操作都在内存中进行。它使用 ethereumjs-vm 实现了这一点,这是 EVM 的 JavaScript 实现。使用 ethereumjs-vm,你可以创建自己的 EVM 并运行字节码。如果需要,我们可以通过提供连接的 URL 来配置浏览器 Solidity 使用以太坊节点。用户界面非常信息丰富;因此,你可以自己尝试所有这些。

Oraclize Web IDE 的特殊之处在于它在内存执行环境中部署了 Oraclize 合约,因此您不必连接到测试网或主网节点,但如果您使用浏览器 Solidity,则必须连接到测试网或主网节点以测试 Oraclize API。

你可以在 dev.Oraclize.it/ 找到更多与集成 Oraclize 相关的资源。

处理字符串

在 Solidity 中处理字符串不像在其他高级编程语言(如 JavaScript、Python 等)中那样容易。因此,许多 Solidity 程序员提出了各种库和合约,以便轻松处理字符串。

strings 库是最流行的字符串实用程序库。它允许我们通过将字符串转换为称为切片的东西来连接、连接、拆分、比较等等。切片是一个结构,它保存了字符串的长度和字符串的地址。由于切片只需指定一个偏移量和一个长度,复制和操作切片要比复制和操作它们引用的字符串便宜得多。

为了进一步减少 gas 成本,大多数需要返回切片的切片函数会修改原始切片,而不是分配一个新的切片;例如,s.split(".") 将返回直到第一个 "." 的文本,并修改 s 以仅包含该 "." 后的字符串。在你不想修改原始切片的情况下,可以使用 .copy() 进行复制,例如,s.copy().split(".")。尽量避免在循环中使用这种习惯用法;由于 Solidity 没有内存管理,这将导致分配许多短暂的切片,稍后会被丢弃。

必须复制字符串数据的函数将返回字符串而不是切片;如果需要,可以将其强制转换回切片以进行进一步处理。

让我们看看使用 strings 库处理字符串的几个示例:

pragma Solidity ⁰.4.0; 

import "github.com/Arachnid/Solidity-stringutils/strings.sol"; 

contract Contract 
{ 
    using strings for *; 

    function Contract() 
    { 
        //convert string to slice 
        var slice = "xyz abc".toSlice(); 

        //length of string 
        var length = slice.len(); 

        //split a string 
        //subslice = xyz 
        //slice = abc 
        var subslice = slice.split(" ".toSlice()); 

        //split a string into an array 
        var s = "www.google.com".toSlice(); 
        var delim = ".".toSlice(); 
        var parts = new string[](s.count(delim)); 
        for(uint i = 0; i < parts.length; i++) { 
            parts[i] = s.split(delim).toString(); 
        } 

        //Converting a slice back to a string 
        var myString = slice.toString(); 

        //Concatenating strings 
        var finalSlice = subslice.concat(slice); 

        //check if two strings are equal 
        if(slice.equals(subslice)) 
        { 

        } 
    } 
}

前述代码是不言自明的。

返回两个切片的函数有两个版本:一个是不分配的版本,它将第二个切片作为参数,直接在原地修改它;另一个是分配并返回第二个切片的版本。例如,让我们来看看以下内容:

var slice1 = "abc".toSlice(); 

//moves the string pointer of slice1 to point to the next rune (letter) 
//and returns a slice containing only the first rune 
var slice2 = slice1.nextRune(); 

var slice3 = "abc".toSlice(); 
var slice4 = "".toSlice(); 

//Extracts the first rune from slice3 into slice4, advancing the slice to point to the next rune and returns slice4\. 
var slice5 = slice3.nextRune(slice4);

您可以在github.com/Arachnid/Solidity-stringutils了解更多关于 strings 库的信息。

构建投注合同

在我们的投注应用程序中,两个人可以选择在一场足球比赛上进行投注,一个人支持主队,另一个人支持客队。他们都应该以相同的金额进行投注,赢家将拿走所有的钱。如果比赛是平局,那么他们两人都将拿回他们的钱。

我们将使用 FastestLiveScores API 来了解比赛的结果。它提供了一个免费的 API,让我们每小时可以免费进行 100 次请求。首先,去创建一个账户,然后生成一个 API 密钥。要创建一个账户,请访问 customer.fastestlivescores.com/register,一旦账户创建完成,您将在 customer.fastestlivescores.com/ 看到 API 密钥。您可以在 docs.crowdscores.com/ 找到 API 文档。

对于我们应用程序中两个人之间的每次投注,都会部署一个投注合同。合同将包含从 FastestLiveScores API 检索到的比赛 ID,双方需要投资的 wei 金额以及双方的地址。一旦双方都在合同中投资了,他们将得知比赛的结果。如果比赛尚未结束,则他们将在每隔 24 小时后尝试检查结果。

这是合同的代码:

pragma Solidity ⁰.4.0; 

import "github.com/Oraclize/Ethereum-api/oraclizeAPI.sol"; 
import "github.com/Arachnid/Solidity-stringutils/strings.sol"; 

contract Betting is usingOraclize 
{ 
    using strings for *; 

    string public matchId; 
    uint public amount; 
    string public url; 

    address public homeBet; 
    address public awayBet; 

    function Betting(string _matchId, uint _amount, string _url)  
    { 
        matchId = _matchId; 
        amount = _amount; 
        url = _url; 

        oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS); 
    } 

    //1 indicates home team 
    //2 indicates away team 
    function betOnTeam(uint team) payable 
    { 

        if(team == 1) 
        { 
            if(homeBet == 0) 
            { 
                if(msg.value == amount) 
                { 
                    homeBet = msg.sender;    
                    if(homeBet != 0 && awayBet != 0) 
                    { 
                        oraclize_query("URL", url); 
                    } 
                } 
                else 
                { 
                    throw; 
                } 
            } 
            else 
            { 
                throw; 
            } 
        } 
        else if(team == 2) 
        { 
            if(awayBet == 0) 
            { 
                if(msg.value == amount) 
                { 
                    awayBet = msg.sender;           

                    if(homeBet != 0 && awayBet != 0) 
                    { 
                        oraclize_query("URL", url); 
                    } 
                } 
                else 
                { 
                    throw; 
                } 
            } 
            else 
            { 
                throw; 
            } 
        } 
        else 
        { 
            throw; 
        } 
    } 

    function __callback(bytes32 myid, string result, bytes proof) { 
        if (msg.sender != oraclize_cbAddress()) 
        { 
            throw;     
        } 
        else 
        { 
            if(result.toSlice().equals("home".toSlice())) 
            { 
                homeBet.send(this.balance); 
            } 
            else if(result.toSlice().equals("away".toSlice())) 
            { 
                awayBet.send(this.balance); 
            } 
            else if(result.toSlice().equals("draw".toSlice())) 
            { 
                homeBet.send(this.balance / 2); 
                awayBet.send(this.balance / 2); 
            } 
            else 
            { 
                if (Oraclize.getPrice("URL") < this.balance)  
                { 
                    oraclize_query(86400, "URL", url); 
                } 
            } 
        } 
    } 
}

合同代码是自说明的。现在使用 solc.js 或浏览器 Solidity 编译上述代码,具体取决于您自己的喜好。您不需要链接 strings 库,因为其中的所有函数都设置为internal可见性。

在浏览器 Solidity 中,当指定从 HTTP URL 导入库或合同时,请确保它是托管在 GitHub 上;否则,它将无法获取。在 GitHub 文件 URL 中,请确保删除协议以及 blob/{branch-name}

为投注合同构建客户端

为了方便查找匹配的 ID、部署和投资于合同,我们需要构建一个 UI 客户端。所以让我们开始构建一个客户端,它将有两条路径,即主页路径用于部署合同和投注比赛,另一条路径用于查找比赛列表。我们将允许用户使用他们自己的离线账户进行部署和投注,以便整个投注过程以分散的方式进行,没有人能作弊。

在我们开始构建客户端之前,请确保您已经同步了测试网络,因为 Oraclize 仅在以太坊的测试网络/主网络上运行,而不在私有网络上运行。您可以切换到测试网络,并通过将--dev选项替换为--testnet选项来开始下载测试网络区块链。例如,看一下以下内容:

geth --testnet --rpc --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545"

规划结构

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

要测试Final目录,您需要在其中运行npm install,然后使用Final目录内的node app.js命令运行应用程序。

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

public目录包含与前端相关的文件。在public/css内,您将找到bootstrap.min.css,这是 bootstrap 库。在public/html内,您将找到index.htmlmatches.ejs文件,您将在其中放置我们应用程序的 HTML 代码,并且在public/js目录内,您将找到 web3.js 和 ethereumjs-tx 的 js 文件。在public/js内,您还会找到一个main.js文件,您将在其中放置我们应用程序的前端 JS 代码。您还将在 Oraclize Python 工具中找到加密查询的内容。

构建后端

首先构建应用程序的后端。首先,在 Initial 目录内运行npm install以安装后端所需的依赖。

这是运行 express 服务并提供index.html文件和静态文件并设置视图引擎的后端代码:

var express = require("express"); 
var app = express(); 

app.set("view engine", "ejs"); 

app.use(express.static("public")); 

app.listen(8080); 

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

上述代码是不言自明的。现在让我们继续进行。我们的应用程序将有另一个页面,其中将显示最近的比赛列表,包括比赛的 ID 和结果(如果比赛已结束)。以下是端点的代码:

var request = require("request"); 
var moment = require("moment"); 

app.get("/matches", function(req, res) { 
    request("https://api.crowdscores.com/v1/matches?api_key=7b7a988932de4eaab4ed1b4dcdc1a82a", function(error, response, body) { 
        if (!error && response.statusCode == 200) { 
            body = JSON.parse(body); 

            for (var i = 0; i < body.length; i++) { 
             body[i].start = moment.unix(body[i].start / 
               1000).format("YYYY MMM DD hh:mm:ss"); 
            } 

            res.render(__dirname + "/public/html/matches.ejs", { 
                matches: body 
            }); 
        } else { 
            res.send("An error occured"); 
        } 
    }) 
})

在这里,我们正在进行 API 请求以获取最近比赛的列表,然后将结果传递给matches.ejs文件,以便它可以在用户友好的 UI 中渲染结果。API 结果以时间戳形式给出比赛开始时间;因此,我们正在使用 moment 将其转换为可读的人类格式。我们从后端而不是从前端发出此请求,以便我们不会向用户公开 API 密钥。

我们的后端将向前端提供 API,通过该 API 前端可以在部署合约之前加密查询。我们的应用程序不会提示用户创建 API 密钥,因为这将是不良的 UX 实践。应用程序的开发人员控制 API 密钥不会造成任何伤害,因为开发人员无法修改 API 服务器的结果;因此,即使应用程序的开发人员知道 API 密钥,用户仍将信任该应用程序。

下面是加密端点的代码:

var PythonShell = require("python-shell"); 

app.get("/getURL", function(req, res) { 
    var matchId = req.query.matchId; 

    var options = { 
        args: ["-e", "-p", "044992e9473b7d90ca54d2886c7addd14a61109af202f1c95e218b0c99eb060c7134c4ae46345d0383ac996185762f04997d6fd6c393c86e4325c469741e64eca9", "json(https://api.crowdscores.com/v1/matches/" + matchId + "?api_key=7b7a988932de4eaab4ed1b4dcdc1a82a).outcome.winner"], 
        scriptPath: __dirname 
    }; 

    PythonShell.run("encrypted_queries_tools.py", options, function 
      (err, results) { 
        if(err) 
        { 
            res.send("An error occured"); 
        } 
        else 
        { 
            res.send(results[0]); 
        } 
    }); 
})

我们已经看到如何使用此工具。为了成功运行此端点,请确保在您的系统上安装了 Python。即使安装了 Python,此端点也可能显示错误,指示未安装 Python 的密码学和 base58 模块。因此,如果工具提示您安装这些模块,请确保您安装了这些模块。

构建前端

现在让我们构建应用程序的前端。我们的前端将让用户看到最近比赛的列表,部署投注合约,对游戏进行投注,并让他们看到有关投注合约的信息。

让我们首先实现matches.ejs文件,该文件将显示最近比赛的列表。这是这个文件的代码:

<!DOCTYPE html>
<html lang="en">
    <head> 
         <meta charset="utf-8"> 
         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
         <meta http-equiv="x-ua-compatible" content="ie=edge"> 
         <link rel="stylesheet" href="/css/bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <br> 
             <div class="row m-t-1"> 
                 <div class="col-md-12"> 
                     <a href="/">Home</a> 
                 </div> 
             </div> 
             <br> 
             <div class="row"> 
                 <div class="col-md-12"> 
                     <table class="table table-inverse"> 
                           <thead> 
                             <tr> 
                                 <th>Match ID</th> 
                                 <th>Start Time</th> 
                                 <th>Home Team</th> 
                                 <th>Away Team</th> 
                                 <th>Winner</th> 
                             </tr> 
                           </thead> 
                           <tbody> 
                               <% for(var i=0; i < matches.length; i++) { %> 
                                   <tr> 
                                       <td><%= matches[i].dbid %></td> 
                                       <% if (matches[i].start) { %> 
                                        <td><%= matches[i].start %></td> 
                                     <% } else { %> 
                                         <td>Time not finalized</td> 
                                     <% } %> 
                                       <td><%= matches[i].homeTeam.name %></td> 
                                       <td><%= matches[i].awayTeam.name %></td> 
                                       <% if (matches[i].outcome) { %> 
                                        <td><%= matches[i].outcome.winner %></td> 
                                     <% } else { %> 
                                         <td>Match not finished</td> 
                                     <% } %> 
                                 </tr> 
                             <% } %> 
                           </tbody> 
                     </table> 
                 </div> 
             </div> 
         </div> 
     </body> 
 </html>

上述代码是不言自明的。现在让我们为我们的首页编写 HTML 代码。我们的首页将显示三个表单。第一个表单用于部署一个投注合约,第二个表单用于投资一个投注合约,第三个表单用于显示已部署投注合约的信息。

这是首页的 HTML 代码。将此代码放在index.html页面中:

<!DOCTYPE html> 
 <html lang="en"> 
     <head> 
         <meta charset="utf-8"> 
         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
         <meta http-equiv="x-ua-compatible" content="ie=edge"> 
         <link rel="stylesheet" href="/css/bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <br> 
             <div class="row m-t-1"> 
                 <div class="col-md-12"> 
                     <a href="/matches">Matches</a> 
                 </div> 
             </div> 
             <br> 
             <div class="row"> 
                 <div class="col-md-4"> 
                     <h3>Deploy betting contract</h3> 
                     <form id="deploy"> 
                         <div class="form-group"> 
                             <label>From address: </label> 
                             <input type="text" class="form-control" id="fromAddress"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Private Key: </label> 
                             <input type="text" class="form-control" id="privateKey"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Match ID: </label> 
                             <input type="text" class="form-control" id="matchId"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Bet Amount (in ether): </label> 
                             <input type="text" class="form-control" id="betAmount"> 
                         </div> 
                         <p id="message" style="word-wrap: break-word"></p> 
                         <input type="submit" value="Deploy" class="btn btn-primary" /> 
                     </form> 
                 </div> 
                 <div class="col-md-4"> 
                     <h3>Bet on a contract</h3> 
                     <form id="bet"> 
                         <div class="form-group"> 
                             <label>From address: </label> 
                             <input type="text" class="form-control" id="fromAddress"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Private Key: </label> 
                             <input type="text" class="form-control" id="privateKey"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Contract Address: </label> 
                             <input type="text" class="form-control"
id="contractAddress"> 
                         </div> 
                         <div class="form-group"> 
                             <label>Team: </label> 
                             <select class="form-control" id="team"> 
                                 <option>Home</option> 
                                 <option>Away</option> 
                             </select> 
                         </div> 
                         <p id="message" style="word-wrap: break-word"></p> 
                         <input type="submit" value="Bet" class="btn btn-primary" /> 
                     </form> 
                 </div> 
                 <div class="col-md-4"> 
                     <h3>Display betting contract</h3> 
                     <form id="find"> 
                         <div class="form-group"> 
                             <label>Contract Address: </label> 
                             <input type="text" class="form-control"  
 d="contractAddress"> 
                         </div> 
                         <p id="message"></p> 
                         <input type="submit" value="Find" class="btn btn-primary" /> 
                     </form> 
                 </div> 
             </div> 
         </div> 

         <script type="text/javascript" src="img/web3.min.js"></script> 
         <script type="text/javascript" src="img/ethereumjs-tx.js"></script> 
         <script type="text/javascript" src="img/main.js"></script> 
     </body> 
</html>

上述代码是不言自明的。现在让我们编写 JavaScript 代码来实际部署合约,在合约上投资,并显示有关合约的信息。以下是所有这些的代码。将此代码放在main.js文件中:

var bettingContractByteCode = "6060604..."; 
var bettingContractABI = [{"constant":false,"inputs":[{"name":"team","type":"uint256"}],"name":"betOnTeam","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"myid","type":"bytes32"},{"name":"result","type":"string"}],"name":"__callback","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"myid","type":"bytes32"},{"name":"result","type":"string"},{"name":"proof","type":"bytes"}],"name":"__callback","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"url","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"matchId","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"amount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"homeBet","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"awayBet","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"_matchId","type":"string"},{"name":"_amount","type":"uint256"},{"name":"_url","type":"string"}],"payable":false,"type":"constructor"}]; 

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

function getAJAXObject() 
{ 
   var request; 
   if (window.XMLHttpRequest) { 
       request = new XMLHttpRequest(); 
   } else if (window.ActiveXObject) { 
       try { 
           request = new ActiveXObject("Msxml2.XMLHTTP"); 
       } catch (e) { 
           try { 
               request = new ActiveXObject("Microsoft.XMLHTTP"); 
           } catch (e) {} 
       } 
   } 

   return request; 
} 

document.getElementById("deploy").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   var fromAddress = document.querySelector("#deploy #fromAddress").value; 
   var privateKey = document.querySelector("#deploy #privateKey").value; 
   var matchId = document.querySelector("#deploy #matchId").value; 
   var betAmount = document.querySelector("#deploy #betAmount").value; 

   var url = "/getURL?matchId=" + matchId; 

   var request = getAJAXObject(); 

   request.open("GET", url); 

   request.onreadystatechange = function() { 
       if (request.readyState == 4) { 
           if (request.status == 200) { 
               if(request.responseText != "An error occured") 
               { 
           var queryURL = request.responseText; 

           var contract = web3.eth.contract(bettingContractABI); 
           var data = contract.new.getData(matchId, 
             web3.toWei(betAmount, "ether"), queryURL, { 
               data: bettingContractByteCode 
                }); 

           var gasRequired = web3.eth.estimateGas({ data: "0x" + data
             }); 

      web3.eth.getTransactionCount(fromAddress, function(error, nonce){ 

       var rawTx = { 
            gasPrice: web3.toHex(web3.eth.gasPrice), 
             gasLimit: web3.toHex(gasRequired), 
              from: fromAddress, 
               nonce: web3.toHex(nonce), 
                data: "0x" + data, 
                 }; 

      privateKey = EthJS.Util.toBuffer(privateKey, "hex"); 

       var tx = new EthJS.Tx(rawTx); 
       tx.sign(privateKey); 

      web3.eth.sendRawTransaction("0x" + 
       tx.serialize().toString("hex"), function(err, hash) { 
            if(!err) 
                {document.querySelector("#deploy #message").
                   innerHTML = "Transaction Hash: " + hash + ". 
                     Transaction is mining..."; 

            var timer = window.setInterval(function(){ 
            web3.eth.getTransactionReceipt(hash, function(err, result){ 
            if(result) 
             {window.clearInterval(timer); 
       document.querySelector("#deploy #message").innerHTML = 
         "Transaction Hash: " + hash + " and contract address is: " + 
             result.contractAddress;} 
               }) 
                }, 10000) 
                 } 
             else 
           {document.querySelector("#deploy #message").innerHTML = err; 
             } 
           }); 
          }) 

          } 
           } 
       } 
   }; 

   request.send(null); 

}, false) 

document.getElementById("bet").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   var fromAddress = document.querySelector("#bet #fromAddress").value; 
   var privateKey = document.querySelector("#bet #privateKey").value; 
   var contractAddress = document.querySelector("#bet #contractAddress").value; 
   var team = document.querySelector("#bet #team").value; 

   if(team == "Home") 
   { 
         team = 1; 
   } 
   else 
   { 
         team = 2; 
   }  

   var contract = web3.eth.contract(bettingContractABI).at(contractAddress); 
   var amount = contract.amount(); 

   var data = contract.betOnTeam.getData(team); 

   var gasRequired = contract.betOnTeam.estimateGas(team, { 
         from: fromAddress, 
         value: amount, 
         to: contractAddress 
   }) 

   web3.eth.getTransactionCount(fromAddress, function(error, nonce){ 

         var rawTx = { 
           gasPrice: web3.toHex(web3.eth.gasPrice), 
           gasLimit: web3.toHex(gasRequired), 
           from: fromAddress, 
           nonce: web3.toHex(nonce), 
           data: data, 
           to: contractAddress, 
           value: web3.toHex(amount) 
       }; 

       privateKey = EthJS.Util.toBuffer(privateKey, "hex"); 

       var tx = new EthJS.Tx(rawTx); 
         tx.sign(privateKey); 

         web3.eth.sendRawTransaction("0x" + tx.serialize().toString("hex"), function(err, hash) { 
               if(!err) 
               { 
    document.querySelector("#bet #message").innerHTML = "Transaction 
      Hash: " + hash; 
        } 
      else 
       { 
       document.querySelector("#bet #message").innerHTML = err; 
      } 
     }) 
   }) 
    }, false) 

document.getElementById("find").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   var contractAddress = document.querySelector("#find 
     #contractAddress").value; 
   var contract =  
      web3.eth.contract(bettingContractABI).at(contractAddress); 

   var matchId = contract.matchId(); 
   var amount = contract.amount(); 
   var homeAddress = contract.homeBet(); 
   var awayAddress = contract.awayBet(); 

   document.querySelector("#find #message").innerHTML = "Contract balance is: " + web3.fromWei(web3.eth.getBalance(contractAddress), "ether") + ", Match ID is: " + matchId + ", bet amount is: " + web3.fromWei(amount, "ether") + " ETH, " + homeAddress + " has placed bet on home team and " + awayAddress + " has placed bet on away team"; 
}, false)

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

  1. 首先,我们将合约的字节码和 ABI 存储在bettingContractByteCodebettingContractABI变量中。

  2. 然后,我们创建一个连接到我们的测试网节点的Web3实例。

  3. 然后,我们有getAJAXObject函数(一个跨浏览器兼容的函数),它返回一个 AJAX 对象。

  4. 然后,我们将submit事件侦听器附加到第一个表单上,该表单用于部署合约。在事件侦听器的回调中,我们通过传递matchId来向getURL端点发出请求,以获取加密的查询字符串。然后,我们生成用于部署合约的数据。然后,我们找出gasRequired。我们使用函数对象的estimateGas方法来计算所需的 gas,但您也可以使用web3.eth.estimateGas方法。它们在参数方面有所不同;即,在前面的情况下,您不需要传递交易数据。请记住,如果函数调用引发异常,estimateGas将返回块 gas 限制。然后,我们计算 nonce。在这里,我们只是使用getTransactionCount方法而不是我们之前学到的实际过程。我们之所以这样做,只是为了简化代码。然后,我们创建原始交易,对其进行签名并广播。一旦交易被挖掘,我们将显示合约地址。

  5. 然后,我们为第二个表单附加了一个submit事件监听器,用于投资合约。在这里,我们生成交易的data部分,计算所需的 gas,创建原始交易,签名并广播。在计算交易所需的 gas 时,我们传递了从账户地址和值对象属性到合约地址,因为它是一个函数调用,而 gas 取决于值、来自地址和合约地址。请记住,在找到调用合约函数所需的 gas 时,您可以传递tofromvalue属性,因为 gas 取决于这些值。

  6. 最后,我们为第三个表单添加了一个submit事件监听器,即显示部署的押注合约的信息。

测试客户端

现在我们已经完成了建立我们的押注平台,是时候测试它了。在测试之前,请确保测试网络区块链已完全下载并正在寻找新的入块。

现在使用我们之前构建的钱包服务,生成三个账户。使用 faucet.ropsten.be:3001/ 为每个账户添加一以太币。

然后,在Initial目录中运行node app.js,然后访问http://localhost:8080/matches,您将看到以下截图中显示的内容:

在这里,您可以复制任何比赛 ID。假设您想使用第一场比赛进行测试,即 123945。现在访问 http://localhost:8080 您将看到以下截图中显示的内容:

现在通过填写第一个表单中的输入字段并单击“部署”按钮来部署合约,如下所示。使用您的第一个账户来部署合约。

现在从第二个账户对合约的主队和从第三个账户对客队进行押注,如下所示:

现在在第三个表单中输入合约地址并单击“查找”按钮以查看合约的详细信息。您将看到类似于以下截图的内容:

一旦两笔交易都被挖掘,再次检查合约的详细信息,您将看到类似于以下截图的内容:

在这里,您可以看到合约没有任何以太币,所有以太币都被转移到押注主队的账户。

总结

在本章中,我们深入学习了 Oraclize 和 strings 库。我们将它们一起使用,构建了一个去中心化的押注平台。现在您可以根据您的需求进一步定制合约和客户端。为了增强应用程序,您可以向合约添加事件并在客户端显示通知。我们的目标是了解去中心化押注应用程序的基本架构。

在下一章中,我们将学习如何使用 truffle 构建企业级以太坊智能合约,通过构建我们自己的加密货币。

第八章:构建企业级智能合约

到目前为止,我们一直在使用浏览器 Solidity 来编写和编译 Solidity 代码。 我们使用 web3.js 来测试我们的合同。 我们也可以使用 Solidity 在线 IDE 进行测试。 因为我们只编译了一个小合同,而且导入的内容很少,所以这似乎没问题。 当您开始构建大型和复杂的智能合约时,您将开始遇到使用当前流程进行编译和测试的问题。 在本章中,我们将学习有关 truffle 的知识,它使得构建企业级 DApps 变得容易,通过构建一个另类币。 除了比特币之外的所有加密货币都被称为另类币。

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

  • 什么是 ethereumjs-testrpc 节点,以及如何使用它?

  • 事件主题是什么?

  • 使用 truffle-contract 包处理合同。

  • 安装 truffle 并探索 truffle 命令行工具和配置文件

  • 使用 truffle 编译、部署和测试 Solidity 代码

  • 通过 NPM 和 EthPM 进行包管理

  • 使用 truffle 控制台和编写外部脚本

  • 使用 truffle 构建 DApp 的客户端

探索 ethereumjs-testrpc

ethereumjs-testrpc 是一个基于 Node.js 的以太坊节点,用于测试和开发。 它模拟完整节点行为,使以太坊应用程序的开发速度加快。 它还包括所有流行的 RPC 函数和功能(例如事件),并且可以以确定性的方式运行,以使开发变得轻松愉快。

它是用 JavaScript 编写的,并作为一个 npm 包分发。 在撰写本文时,最新版本的 ethereumjs-testrpc 是 3.0.3,至少需要 Node.js 版本 6.9.1 才能正常运行。

它将所有内容保存在内存中; 因此,每次节点重新启动时,它都会丢失先前的状态。

安装和使用

有三种方法可以使用 ethereumjs-testrpc 模拟以太坊节点。 每种方法都有自己的用例。 让我们来探索一下。

testrpc 命令行应用程序

testrpc 命令可用于模拟以太坊节点。 要安装此命令行应用程序,您需要全局安装 ethereumjs-testrpc

npm install -g ethereumjs-testrpc

这里是可以提供的各种选项:

  • -a--accounts:这指定了启动时要生成的帐户数。

  • -b--blocktime:这指定了自动挖矿的秒数。 默认值为 0,没有自动挖矿。

  • -d--deterministic:每次运行节点时,它将生成 10 个确定性地址; 也就是说,当您提供此标志时,每次都会生成相同的地址集。 这个选项还可以根据预定义的助记词生成确定性地址。

  • -n--secure:默认情况下锁定可用帐户。 当此选项在不带 --unlock 选项的情况下使用时,将不会创建 HD 钱包。

  • -m--mnemonic:使用特定的 HD 钱包助记词来生成初始地址。

  • -p--port:要监听的端口号。默认为 8545。

  • -h--hostname:要监听的主机名。默认为 Node 的server.listen()默认值。

  • -s--seed:生成 HD 钱包助记词所需的任意数据。

  • -g--gasPrice:使用自定义的 gas 价格(默认为 1)。如果在向节点发送交易时未提供 gas 价格,则将使用此 gas 价格。

  • -l--gasLimit:使用自定义的 gas 限制(默认为 0x47E7C4)。如果在向节点发送交易时未提供 gas 限制,则将使用此 gas 限制。

  • -f--fork:这是从另一个当前运行的以太坊节点在给定块中分叉。输入应该是其他客户端的 HTTP 位置和端口;例如,http://localhost:8545。可以选择指定要从中分叉的块,使用@标记:http://localhost:8545@1599200

  • --debug:输出用于调试的虚拟机操作码。

  • --account:此选项用于导入账户。它可以任意多次指定--account=...,传递任意的私钥和它们关联的余额来生成初始地址。在使用--account时,不会为你创建一个 HD 钱包。

  • -u--unlock:指定--unlock ...任意次数,传递要解锁的特定账户的地址或账户索引。当与--secure同时使用时,--unlock将覆盖指定帐户的锁定状态:testrpc --secure --unlock "0x1234..." --unlock "0xabcd..."。还可以通过编号指定解锁账户:testrpc --secure -u 0 -u 1。该功能也可以用于冒充帐户和解锁您原本无法访问的地址。在与--fork功能一起使用时,可以使用testrpc以任何区块链上的地址进行交易,这在测试和动态分析中非常有用。

  • --networkId:用于指定此节点所属的网络 ID。

请注意,私钥长度为 64 个字符,必须输入为带有 0x 前缀的十六进制字符串。余额可以输入为整数,也可以输入为以太币金额的 0x 前缀十六进制值。

ethereumjs-testrpc用作 web3 提供者或作为 HTTP 服务器

你可以像这样将ethereumjs-testrpc用作web3提供者:

var TestRPC = require("ethereumjs-testrpc"); 
web3.setProvider(TestRPC.provider());

你可以像这样将ethereumjs-testrpc用作通用 HTTP 服务器:

var TestRPC = require("ethereumjs-testrpc"); 
var server = TestRPC.server(); 
server.listen(port, function(err, blockchain) {});

provider()server()都接受一个允许您指定ethereumjs-testrpc行为的对象。此参数是可选的。可用选项如下:

  • accounts:值是对象的数组。每个对象都应该有一个带有十六进制值的余额键。也可以指定 secretKey 键,它表示帐户的私钥。如果没有 secretKey,地址将根据给定的余额自动生成。如果指定,密钥用于确定帐户的地址。

  • debug: 输出用于调试的 VM 操作码。

  • logger: 值是实现 log() 函数的对象。

  • mnemonic: 使用特定的 HD 钱包助记词生成初始地址。

  • port: 作为服务器运行时要监听的端口号。

  • seed: 生成 HD 钱包助记词所需的任意数据。

  • total_accounts: 启动时要生成的帐户数。

  • fork: 与前述 --fork 选项相同。

  • network_id: 与 --networkId 选项相同。用于指定此节点所属的网络 ID。

  • time: 第一个区块应该开始的日期。与 evm_increaseTime 方法一起使用此功能来测试“依赖于时间”的代码。

  • locked: 指定默认情况下是否锁定帐户。

  • unlocked_accounts: 一个指定应解锁哪些帐户的地址或地址索引的数组。

可用的 RPC 方法

这是使用 ethereumjs-testrpc 提供的 RPC 方法列表:

  • eth_accounts

  • eth_blockNumber

  • eth_call

  • eth_coinbase

  • eth_compileSolidity

  • eth_estimateGas

  • eth_gasPrice

  • eth_getBalance

  • eth_getBlockByNumber

  • eth_getBlockByHash

  • eth_getBlockTransactionCountByHash

  • eth_getBlockTransactionCountByNumber

  • eth_getCode (仅支持区块号“latest”)

  • eth_getCompilers

  • eth_getFilterChanges

  • eth_getFilterLogs

  • eth_getLogs

  • eth_getStorageAt

  • eth_getTransactionByHash

  • eth_getTransactionByBlockHashAndIndex

  • eth_getTransactionByBlockNumberAndIndex

  • eth_getTransactionCount

  • eth_getTransactionReceipt

  • eth_hashrate

  • eth_mining

  • eth_newBlockFilter

  • eth_newFilter (包括日志/事件过滤器)

  • eth_sendTransaction

  • eth_sendRawTransaction

  • eth_sign

  • eth_syncing

  • eth_uninstallFilter

  • net_listening

  • net_peerCount

  • net_version

  • miner_start

  • miner_stop

  • rpc_modules

  • web3_clientVersion

  • web3_sha3

还有一些特殊的非标准方法未包含在原始 RPC 规范中:

  • evm_snapshot: 在当前区块中快照区块链的状态。不带参数。返回创建的快照的整数 ID。

  • evm_revert: 将区块链状态恢复到先前的快照。接受一个参数,即要恢复到的快照 ID。如果未传递快照 ID,则将恢复到最新的快照。返回 true。

  • evm_increaseTime: 向前跳转时间。带一个参数,即要增加的秒数。返回秒数的总时间调整。

  • evm_mine: 强制挖掘一个区块。不带参数。独立于挖矿是否启动或停止而挖掘一个区块。

事件主题是什么?

主题是用于索引事件的值。您不能没有主题搜索事件。每当调用事件时,都会生成一个默认主题,该主题被视为事件的第一个主题。一个事件最多可以有四个主题。主题始终以相同的顺序生成。您可以使用一个或多个主题搜索事件。

第一个主题是事件的签名。剩下的三个主题是索引参数的值。如果索引参数是stringbytesarray,那么其 keccak-256 哈希就是主题。

让我们举一个示例来理解主题。假设有这样一个事件:

event ping(string indexed a, int indexed b, uint256 indexed c, string d, int e); 

//invocation of event 
ping("Random String", 12, 23, "Random String", 45);

这里生成了四个主题。它们分别是:

  • 0xb62a11697c0f56e93f3957c088d492b505b9edd7fb6e7872a93b41cdb2020644:这是第一个主题。它是使用web3.sha3("ping(string,int256,uint256,string,int256)")生成的。在这里,您可以看到所有类型都是规范形式的。

  • 0x30ee7c926ebaf578d95b278d78bc0cde445887b0638870a26dcab901ba21d3f2:这是第二个主题。它是使用web3.sha3("随机字符串")生成的。

  • 第三和第四个主题分别为0x000000000000000000000000000000000000000000000000000000000000000c0x0000000000000000000000000000000000000000000000000000000000000017,也就是值的十六进制表示。它们分别使用EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(12, 32))EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(23, 32))计算得出。

在内部,您的以太坊节点将使用主题构建索引,以便您可以轻松找到基于签名和索引值的事件。

假设您想获取前述事件的事件调用,其中第一个参数是随机字符串,第三个参数是2378;那么,您可以通过以下方式使用web3.eth.getFilter找到它们:

var filter = web3.eth.filter({ 
   fromBlock: 0, 
   toBlock: "latest", 
   address: "0x853cdcb4af7a6995808308b08bb78a74de1ef899", 
   topics: ["0xb62a11697c0f56e93f3957c088d492b505b9edd7fb6e7872a93b41cdb2020644", "0x30ee7c926ebaf578d95b278d78bc0cde445887b0638870a26dcab901ba21d3f2", null, [EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(23, 32)), EthJS.Util.bufferToHex(EthJS.Util.setLengthLeft(78, 32))]]            
}); 

filter.get(function(error, result){ 
 if (!error) 
   console.log(result); 
});

因此,我们在这里要求节点返回所有已被0x853cdcb4af7a6995808308b08bb78a74de1ef899合约地址触发的来自区块链的事件,其第一个主题为0xb62a11697c0f56e93f3957c088d492b505b9edd7fb6e7872a93b41cdb2020644,第二个主题为0x30ee7c926ebaf578d95b278d78bc0cde445887b0638870a26dcab901ba21d3f2,第三个主题为0x00000000000000000000000000000000000000000000000000000000000000170x000000000000000000000000000000000000000000000000000000000000004e

在上述代码中,请注意topics数组值的顺序。顺序很重要。

开始使用 truffle-contract

在学习 truffle 之前,学习truffle-contract非常重要,因为truffle-contract与 truffle 紧密集成。 truffle 测试、与 truffle 中的合约交互的代码、部署代码等都是使用truffle-contract编写的。

truffle-contract API 是一个 JavaScript 和 Node.js 库,它使得与以太坊智能合约的交互变得容易。到目前为止,我们一直在使用 web3.js 来部署和调用智能合约函数,这很好,但truffle-contract旨在使与以太坊智能合约的交互变得更加容易。下面是一些truffle-contract的特点,这使得它在处理智能合约方面比 web3.js 更优秀:

  • 同步事务以更好地控制流程(即,直到您保证它们已经被挖掘,事务才会完成)。

  • 基于 Promise 的 API。不再有回调地狱。与 ES6 和 async/await 很好地配合使用。

  • 交易的默认值,例如来自 addressgas

  • 返回每个同步交易的日志、交易收据和交易哈希。

在我们开始使用 truffle-contract 之前,你需要知道它不允许我们使用存储在以太坊节点之外的账户来签署交易;也就是说,它没有类似于 sendRawTransaction 的功能。truffle-contract API 假设你的 DApp 的每个用户都有自己的以太坊节点运行,并且他们的账户存储在该节点中。实际上,这就是 DApp 应该工作的方式,因为如果每个 DApp 的客户端都开始让用户创建和管理账户,那么用户将需要管理这么多账户,并且对于开发人员来说,每次构建客户端都要为每个客户端开发钱包管理器都是痛苦的。现在,问题是客户端如何知道用户在哪里存储账户以及以什么格式?因此,出于可移植性的考虑,建议你假设用户的账户存储在他们的个人节点中,并且为了管理账户,他们使用类似以太坊钱包应用的东西。由于以太坊节点中存储的账户由以太坊节点本身签名,因此不再需要 sendRawTransaction。每个用户都需要有自己的节点,并且不能共享节点,因为当一个账户被解锁时,任何人都可以使用它,这将使用户能够窃取他人的以太币并从他人的账户中进行交易。

如果你使用的应用需要你托管自己的节点并在其中管理账户,请确保不允许所有人对该节点进行 JSON-RPC 调用;相反,只有本地应用程序才能进行调用。另外,请确保不要将账户长时间保持解锁状态,并在不再需要账户时立即将其锁定。

如果你的应用程序需要创建和签署原始交易的功能,那么你可以使用 truffle-contract 来开发和测试智能合约,而在你的应用程序中,你可以像之前一样与合约交互。

安装和引入 truffle-contract

在撰写本文时,最新版本的 truffle-contract API 是 1.1.10。在引入 truffle-contract 之前,你需要先引入 web3.js,因为你需要创建一个提供程序来与 truffle-contract API 一起工作,这样 truffle-contract 将在内部使用提供程序来进行 JSON-RPC 调用。

要在 Node.js 应用中安装 truffle-contract,只需在应用目录中运行以下命令:

npm install truffle-contract

然后使用以下代码进行引入:

var TruffleContract = require("truffle-contract");

若要在浏览器中使用 truffle-contract,你可以在 github.com/trufflesuite/truffle-contract 仓库的 dist 目录中找到浏览器分发版。

在 HTML 中,你可以这样排队:

<script type="text/javascript" src="img/truffle-contract.min.js"></script>

现在你将拥有一个全局变量 TruffleContract

建立一个测试环境

在开始学习关于truffle-contract API 之前,我们需要建立一个测试环境,这将帮助我们在学习过程中测试我们的代码。

首先,通过仅运行testrpc --networkId 10命令来运行代表网络 ID 10 的ethereumjs-testrpc节点。我们随机选择了网络 ID 10 用于开发目的,但您可以自由选择其他网络 ID。只需确保它不是 1,因为主网始终用于实时应用程序,而不用于开发和测试目的。

然后,创建一个 HTML 文件并将此代码放入其中:

<!doctype html> 
<html> 
   <body> 
         <script type="text/javascript" src="img/web3.min.js"></script> 
         <script type="text/javascript" src="img/>            contract.min.js"></script> 
         <script type="text/javascript"> 
               //place your code here 
         </script> 
   </body> 
</html>

下载web3.min.jstruffle-contract.min.js。您可以在github.com/trufflesuite/truffle-contract/tree/master/dist找到truffle-contract的浏览器构建。

truffle-contract API

现在让我们来探索truffle-contract的 API。基本上,truffle-contract有两个 API,即合同抽象 API 和合同实例 API。合同抽象 API 表示关于合同(或库)的各种信息,例如其 ABI;未链接的字节码;如果合同已部署,则其在各个以太坊网络中的地址;如果已部署,则其依赖的库在各个以太坊网络中的地址;以及合同的事件。抽象 API 是为所有合同抽象存在的一组函数。合同实例表示特定网络中部署的合同。实例 API 是向合同实例提供的 API。它是根据您的Solidity源文件中可用的函数动态创建的。针对特定合同的合同实例是通过表示相同合同的合同抽象创建的。

合同抽象 API

合同抽象 API 是使truffle-contract与 web3.js 相比非常特殊的东西。这里是它的特殊之处:

  • 它将根据连接的网络自动获取默认值,如库地址、合同地址等,因此每次更改网络时都不必编辑源代码。

  • 您可以选择只在某些网络中监听某些事件。

  • 它使在运行时轻松将库链接到合同的字节码。一旦您探索了如何使用 API,您将发现还有其他几个好处。

在讨论如何创建合同抽象及其方法之前,让我们编写一个样本合同,该合同抽象将代表。这是样本合同:

pragma Solidity ⁰.4.0; 

import "github.com/pipermerriam/ethereum-string-utils/contracts/StringLib.sol"; 

contract Sample 
{ 
    using StringLib for *; 

    event ping(string status); 

    function Sample() 
    { 
        uint a = 23; 
        bytes32 b = a.uintToBytes(); 

        bytes32 c = "12"; 
        uint d = c.bytesToUInt(); 

        ping("Conversion Done"); 
    } 
}

该合同将uint转换为bytes32,并使用StringLib库将bytes32转换为uintStringLib在主网络上的地址为0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4,但在其他网络上,我们需要部署以测试合同。在继续之前,请使用浏览器 Solidity 编译它,因为您将需要 ABI 和字节码。

现在让我们创建一个代表Sample合同和StringLib库的合同抽象。这是代码,请将其放入HTML文件中:

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

var SampleContract = TruffleContract({ 
   abi: [{"inputs":[],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"status","type":"string"}],"name":"ping","type":"event"}], 
   unlinked_binary: "6060604052341561000c57fe5b5b6000600060006000601793508373__StringLib__6394e8767d90916000604051602001526040518263ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018082815260200191505060206040518083038186803b151561008b57fe5b60325a03f4151561009857fe5b5050506040518051905092507f31320000000000000000000000000000000000000000000000000000000000009150816000191673__StringLib__6381a33a6f90916000604051602001526040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180826000191660001916815260200191505060206040518083038186803b151561014557fe5b60325a03f4151561015257fe5b5050506040518051905090507f3adb191b3dee3c3ccbe8c657275f608902f13e3a020028b12c0d825510439e5660405180806020018281038252600f8152602001807f436f6e76657273696f6e20446f6e65000000000000000000000000000000000081525060200191505060405180910390a15b505050505b6033806101da6000396000f30060606040525bfe00a165627a7a7230582056ebda5c1e4ba935e5ad61a271ce8d59c95e0e4bca4ad20e7f07d804801e95c60029", 
   networks: { 
         1: { 
         links: { 
       "StringLib": "0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4" 
               }, 
        events: { 
                  "0x3adb191b3dee3c3ccbe8c657275f608902f13e3a020028b12c0d825510439e56": { 
                "anonymous": false, 
                "inputs": [ 
                         { 
                          "indexed": false, 
                          "name": "status", 
                          "type": "string" 
                         } 
                           ], 
                           "name": "ping", 
                           "type": "event" 
                     } 
               } 
         }, 
         10: { 
               events: { 
                  "0x3adb191b3dee3c3ccbe8c657275f608902f13e3a020028b12c0d825510439e56": { 
                           "anonymous": false, 
                           "inputs": [ 
                                 { 
                                       "indexed": false, 
                                       "name": "status", 
                                       "type": "string" 
                                 } 
                           ], 
                           "name": "ping", 
                           "type": "event" 
                     } 
               } 
         } 
   }, 
   contract_name: "SampleContract", 
}); 

SampleContract.setProvider(provider); 
SampleContract.detectNetwork(); 

SampleContract.defaults({ 
   from: web3.eth.accounts[0], 
   gas: "900000", 
   gasPrice: web3.eth.gasPrice, 
}) 

var StringLib = TruffleContract({ 
   abi: [{"constant":true,"inputs":[{"name":"v","type":"bytes32"}],"name":"bytesToUInt","outputs":[{"name":"ret","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"v","type":"uint256"}],"name":"uintToBytes","outputs":[{"name":"ret","type":"bytes32"}],"payable":false,"type":"function"}], 
   unlinked_binary: "6060604052341561000c57fe5b5b6102178061001c6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806381a33a6f1461004657806394e8767d14610076575bfe5b6100606004808035600019169060200190919050506100aa565b6040518082815260200191505060405180910390f35b61008c6004808035906020019091905050610140565b60405180826000191660001916815260200191505060405180910390f35b6000600060006000600102846000191614156100c557610000565b600090505b60208110156101355760ff81601f0360080260020a85600190048115156100ed57fe5b0416915060008214156100ff57610135565b603082108061010e5750603982115b1561011857610000565b5b600a8302925060308203830192505b80806001019150506100ca565b8292505b5050919050565b60006000821415610173577f300000000000000000000000000000000000000000000000000000000000000090506101e2565b5b60008211156101e157610100816001900481151561018e57fe5b0460010290507f01000000000000000000000000000000000000000000000000000000000000006030600a848115156101c357fe5b06010260010281179050600a828115156101d957fe5b049150610174565b5b8090505b9190505600a165627a7a72305820d2897c98df4e1a3a71aefc5c486aed29c47c80cfe77e38328ef5f4cb5efcf2f10029", 
   networks: { 
         1: { 
               address: "0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4" 
         } 
   }, 
   contract_name: "StringLib", 
}) 

StringLib.setProvider(provider); 
StringLib.detectNetwork(); 

StringLib.defaults({ 
   from: web3.eth.accounts[0], 
   gas: "900000", 
   gasPrice: web3.eth.gasPrice, 
})

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

  1. 首先,我们创建一个提供者。使用此提供者,truffle-contract将与节点通信。

  2. 然后,我们为Sample合同创建一个合同抽象。要创建合同抽象,我们使用TruffleContract函数。此函数接受一个包含有关合同的各种信息的对象。此对象可以称为工件对象。abiunlinked_binary属性是强制的。对象的其他属性是可选的。abi属性指向合同的 ABI,而unlinked_binary属性指向合同的未链接二进制代码。

  3. 然后,我们有一个网络属性,指示各种网络中有关合同的信息。在这里,我们说在网络 ID 1 中,StringLib依赖项部署在0xcca8353a18e7ab7b3d094ee1f9ddc91bdf2ca6a4地址,因此在网络 1 中部署Sample合同时,它会自动链接它。在网络对象下,我们还可以放置一个address属性,指示合同已部署到此网络并且这是合同地址。在networks对象中还有一个events对象,指定我们感兴趣捕获的合同事件。events对象的键是事件的主题,而values是事件的 ABI。

  4. 然后,我们通过传递新的提供者实例调用SampleContract对象的setProvider方法。这是传递提供者的一种方式,以便truffle-contract可以与节点通信。truffle-contract API 不提供全局设置提供者的方法;相反,您需要为每个合同抽象设置提供者。这是一个功能,使我们可以轻松连接和处理多个网络。

  5. 然后,我们调用SampleContract对象的detectNetwork方法。这是设置合同抽象当前表示的网络 ID 的方法;也就是说,在合同抽象的所有操作中,使用映射到此网络 ID 的值。此方法将自动检测我们的节点连接到哪个网络 ID,并将其自动设置。如果您想要手动设置网络 ID 或在运行时更改它,则可以使用SampleContract.setNetwork(network_id)。如果更改网络 ID,则确保提供者也指向相同网络的节点,因为否则truffle-contract将无法将网络 ID 与正确的链接、地址和事件进行映射。

  6. 然后,我们为SampleContract进行了交易的默认值设置。此方法获取并可选地设置交易默认值。如果没有传递任何参数调用,它将简单地返回表示当前默认值的对象。如果传递了对象,则将设置新的默认值。

  7. 我们对StringLib库执行了相同的操作,以便为其创建一个合约抽象。

创建合约实例

合约实例表示特定网络中部署的合约。使用合约抽象实例,我们需要创建合约实例。创建合约实例有三种方法:

  • SampleContract.new([arg1, arg2, ...], [tx params]): 此函数接受您的合约所需的任何构造函数参数,并将合约的新实例部署到合约抽象设置要使用的网络中。最后一个参数是可选的,您可以使用它来传递事务参数,包括事务来自地址、燃气限制和燃气价格。此函数返回一个承诺,当交易被挖掘时,它会解析为新部署地址处的合约抽象实例。此方法不会对合约抽象所代表的构件对象进行任何更改。在使用此方法之前,请确保它能找到字节码所依赖的库的地址,以供设置要使用的网络。

  • SampleContract.at(address): 此函数创建一个新的合约抽象实例,表示传入地址处的合约。它返回一个“可被解析的”对象(出于向后兼容性,尚未实际承诺)。在确保代码存在于指定地址所在的网络之后,它会解析为一个合约抽象实例。

  • SampleContract.deployed(): 这就像at()一样,但地址是从构件对象中检索的。像at()一样,deployed()是可解析的,并且将解析为代表部署合约的合约实例,以确保代码存在于该位置,并且地址存在于设置为使用的网络上。

让我们部署并获取Sample合约的合约实例。在网络 ID 10中,我们需要使用new()首先部署StringLib库,然后将StringLib库的部署地址添加到StringLib抽象中,将StringLib抽象链接到SampleContract抽象,然后使用new()部署Sample合约以获取Sample合约的实例。但在网络 ID 1中,我们只需要部署SampleContract并获取其实例,因为我们已经在那里部署了StringLib。这是执行所有这些操作的代码:

web3.version.getNetwork(function(err, network_id) { 
   if(network_id == 1) 
   { 
         var SampleContract_Instance = null; 

         SampleContract.new().then(function(instance){ 
               SampleContract.networks[SampleContract.network_id]
                  ["address"] = instance.address; 
               SampleContract_Instance = instance; 
         }) 
   } 
   else if(network_id == 10) 
   { 
         var StringLib_Instance = null; 
         var SampleContract_Instance = null; 

         StringLib.new().then(function(instance){ 
               StringLib_Instance = instance; 
         }).then(function(){ 
               StringLib.networks[StringLib.network_id] = {}; 
               StringLib.networks[StringLib.network_id]["address"] = 
                 StringLib_Instance.address; 
               SampleContract.link(StringLib); 
         }).then(function(result){ 
               return SampleContract.new(); 
         }).then(function(instance){ 
               SampleContract.networks[SampleContract.network_id]
                 ["address"] = instance.address; 
               SampleContract_Instance = instance; 
         }) 
   } 
});

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

  1. 首先,我们检测网络 ID。如果网络 ID 是10,那么我们部署合约和库,如果网络 ID 是10,那么我们只部署合约。

  2. 网络 ID 10,我们部署了 StringLib 合同并获得了它的合同实例。

  3. 然后,我们更新 StringLib 抽象,以便它知道当前网络中合同的地址。更新抽象的界面类似于直接更新 artifacts 对象。如果你连接到网络 ID 1,那么它将覆盖已设置的 StringLib 地址。

  4. 然后,我们将部署的 StringLib 链接到 SampleContract 抽象中。链接将更新链接并将库的事件复制到该抽象当前网络所代表的 SampleContract。库可以被多次链接,并且会覆盖其先前的链接。

  5. 我们将 SampleContract 部署到当前网络。

  6. 我们更新 SampleContract 抽象以存储在其代表的当前网络中合同的地址,以便以后可以使用 deployed() 获取实例。

  7. 在网络 ID 1 的情况下,我们只部署 SampleContract 就可以了。

  8. 现在你只需简单地更改连接的网络并重新启动你的应用程序,你的应用程序就会相应地运行。例如,在开发者的机器上,应用程序将连接到开发网络,而在生产服务器上,它将连接到主网络。显然,你可能不希望每次运行前述文件时都部署合同,因此在部署合同后,你可以实际上更新 artifacts 对象并在代码中检查合同是否已部署。如果尚未部署,则只需部署。而不是手动更新 artifacts 对象,你可以将 artifacts 存储在数据库中或文件中,并编写代码在合同部署完成后自动更新它们。

合同实例 API

每个合同实例都是基于 Solidity 合同源不同,并且 API 是动态创建的。以下是合同实例的各种 API:

  • allEvents:这是合同实例的一个函数,它接受一个回调,每当合同在当前网络 ID 下触发符合事件签名的事件时就会调用。你也可以使用特定事件名的函数来捕捉特定事件,而不是全部事件。在前述合同中,要捕捉 ping 事件,你可以使用 SampleContract_Instance.ping(function(e, r){})

  • send:此函数用于向合同发送以太币。它接受两个参数;即第一个参数是要传输的 wei 数量,第二个参数是一个可选对象,可用于设置交易的 from,指示以太币是从哪个地址发送的。此调用返回一个 promise,并且当其被挖掘时,该 promise 解析为交易的详细信息。

  • 我们可以使用 SampleContract.functionName()SampleContract.functionName.call() 调用合约的任何方法。前者发送交易,而后者仅在 EVM 上调用方法,并且更改不是持久的。这两种方法都返回一个 promise。在第一种情况下,promise 解析为交易的结果,即一个包含交易哈希、日志和交易收据的对象。而在第二种情况下,它解析为方法 call 的返回值。这两种方法都接受函数参数和一个可选的最后一个参数,该参数是一个对象,用于设置交易的 fromgasvalue 等。

Truffle 简介

Truffle 是一个开发环境(提供命令行工具来编译、部署、测试和构建)、框架(提供各种包来编写测试、部署代码、构建客户端等)和资产管道(发布包并使用其他人发布的包)来构建基于以太坊的 DApps。

安装 Truffle

Truffle 可在 OS X、Linux 和 Windows 上运行。Truffle 要求你安装 Node.js 版本 5.0+。在撰写本文时,Truffle 的最新稳定版本是 3.1.2,我们将使用此版本。要安装 Truffle,只需运行以下命令:

npm install -g truffle

在继续之前,请确保你正在运行 network ID 为 10 的 testrpc。原因与前面讨论的相同。

Truffle 初始化

首先,你需要为你的应用创建一个目录。将目录命名为altcoin。在altcoin目录内,运行以下命令来初始化你的项目:

truffle init

完成后,你将得到一个项目结构,其中包括以下项目:

  • contracts:Truffle 期望找到 Solidity 合约的目录。

  • migrations:包含合约部署代码的文件目录。

  • test:用于测试智能合约的测试文件位置。

  • truffle.js:主要的 Truffle 配置文件。

默认情况下,truffle init 会为你提供一组示例合约(MetaCoinConvertLib),它们就像是建立在以太坊之上的一个简单的代币。

这是 MetaCoin 智能合约的源代码,仅供参考:

pragma Solidity ⁰.4.4; 

import "./ConvertLib.sol"; 

contract MetaCoin { 
   mapping (address => uint) balances; 

   event Transfer(address indexed _from, address indexed _to, uint256 _value); 

   function MetaCoin() { 
         balances[tx.origin] = 10000; 
   } 

   function sendCoin(address receiver, uint amount) returns(bool sufficient) { 
         if (balances[msg.sender] < amount) return false; 
         balances[msg.sender] -= amount; 
         balances[receiver] += amount; 
         Transfer(msg.sender, receiver, amount); 
         return true; 
   } 

   function getBalanceInEth(address addr) returns(uint){ 
         return ConvertLib.convert(getBalance(addr),2); 
   } 

   function getBalance(address addr) returns(uint) { 
         return balances[addr]; 
   } 
}

MetaCoin 将 10 k metacoins 分配给部署合约的账户地址。10 k 是存在的比特币的总量。现在这个用户可以使用 sendCoin() 函数将这些 metacoins 发送给任何人。你可以随时使用 getBalance() 来查找你的账户余额。假设一个 metacoin 等于两个以太币,你可以使用 getBalanceInEth() 获取以太币的余额。

ConvertLib 库用于计算以太中 metacoins 的价值。为此,它提供了 convert() 方法。

合约编译

在 Truffle 中编译合约会生成带有 abiunlinked_binary 设置的构建对象。要编译,请运行以下命令:

truffle compile 

Truffle 只会编译自上次编译以来发生变化的合约,以避免不必要的编译。如果你想要覆盖这种行为,可以在前面的命令中加上--all选项。

你可以在build/contracts目录中找到这些构建文件。你可以根据需要自由编辑这些文件。这些文件在运行compilemigrate命令时会被修改。

在编译之前,以下是需要注意的几件事:

  • Truffle 期望你的合约文件定义与其文件名完全匹配的合约。例如,如果你有一个名为MyContract.sol的文件,那么合约文件中应该存在其中一个:contract MyContract{}library myContract{}

  • 文件名匹配区分大小写,这意味着如果你的文件名没有大写,那么你的合约名也不应该有大写。

  • 你可以使用 Solidity 的import命令声明合约依赖关系。Truffle 将按照正确的顺序编译合约,并在必要时自动链接库。依赖关系必须相对于当前 Solidity 文件指定,并以./../开头。

Truffle 版本 3.1.2 使用的编译器版本是 0.4.8。目前 Truffle 不支持更改编译器版本,因此是固定的。

配置文件

truffle.js文件是用于配置项目的 JavaScript 文件。该文件可以执行任何必要的代码来创建项目的配置。它必须导出一个代表你的项目配置的对象。以下是文件的默认内容:

module.exports = { 
  networks: { 
    development: { 
      host: "localhost", 
      port: 8545, 
      network_id: "*" // Match any network id 
    } 
  } 
};

此对象可以包含各种属性。但最基本的一个是networksnetworks属性指定了哪些网络可用于部署,以及在与每个网络交互时的特定交易参数(例如gasPricefromgas等)。默认的gasPrice是 100,000,000,000,gas是 4712388,from是以太坊客户端中的第一个可用合约。

你可以指定任意数量的网络。请编辑配置文件为以下内容:

module.exports = { 
  networks: { 
    development: { 
      host: "localhost", 
      port: 8545, 
      network_id: "10" 
    }, 
    live: { 
         host: "localhost", 
      port: 8545, 
      network_id: "1" 
    } 
  } 
};

在上面的代码中,我们定义了两个名为developmentlive的网络。

当在 Windows 上使用命令提示符时,默认的配置文件名称可能会与truffle可执行文件发生冲突。如果是这种情况,我们建议您使用 Windows PowerShell 或 Git BASH,因为这些 shell 不会出现此冲突。或者,您可以将配置文件重命名为truffle-config.js以避免此冲突。

部署合约

即使是最小的项目也会与至少两个区块链进行交互:一个是开发者机器上的,比如 EthereumJS TestRPC,另一个代表着开发者最终将部署其应用程序的网络(例如主 Ethereum 网络或私有联盟网络)。

因为网络在运行时由合约抽象自动检测,这意味着您只需要部署一次您的应用或前端。当您的应用运行时,运行的以太坊客户端将确定使用哪些 artifacts,并且这将使您的应用非常灵活。

包含将合约部署到以太坊网络的代码的 JavaScript 文件称为迁移。这些文件负责分阶段执行部署任务,并且根据您的部署需求会随着时间而变化。随着项目的发展,您将创建新的迁移脚本以在区块链上进一步发展。先前运行的迁移历史记录在区块链上通过特殊的 Migrations 合约记录。如果您已经看过 contractsbuild/contracts 目录的内容,那么您会注意到 Migrations 合约的存在。该合约应始终存在,并且除非您知道自己在做什么,否则不应触摸它。

迁移文件

在迁移目录中,您会注意到文件名以数字为前缀;也就是说,您会找到 1_initial_migration.js2_deploy_contracts.js 文件。要记录迁移是否成功运行,需要带有编号前缀。

Migrations 合约(存储在 last_completed_migration 中)存储着对应于 migrations 文件夹中找到的最后一个应用的迁移脚本的编号。Migrations 合约总是首先部署。编号约定为 x_script_name.js,其中 x 从 1 开始。你的应用合约通常从编号为 2 的脚本开始。

因此,由于 Migrations 合约存储了应用的最后部署脚本的编号,truffle 将不会再次运行这些脚本。另一方面,未来,您的应用可能需要部署修改过的或新的合约。为了实现这一点,您会创建一个增加编号的新脚本,描述需要发生的步骤。然后,再次运行一次后,它们将不会再次运行。

编写迁移

在迁移文件的开头,我们通过 artifacts.require() 方法告诉 truffle 我们想要与之交互的合约。这个方法类似于 Node 的 require,但在我们的情况下,它具体返回一个合约抽象,我们可以在部署脚本的其余部分中使用它。

所有迁移必须通过 module.exports 语法导出一个函数。每个迁移导出的函数应将一个 deployer 对象作为其第一个参数。该对象通过提供清晰的 API 来部署智能合约,并执行一些部署的更普通的职责,例如保存部署后的 artifacts 文件以供以后使用,链接库等,来协助部署。deployer 对象是您进行部署任务分阶段的主要接口。

这里是 deployer 对象的方法。所有方法都是同步的:

  • deployer.deploy(contractAbstraction, args..., options): 部署由合约抽象对象指定的特定合约,可选择传入构造函数参数。对于单例合约非常有用,这样您的 DApp 只存在一个该合约的实例。这将在部署后设置合约的地址(也就是 artifacts 文件中的address属性将等于新部署的地址),并覆盖存储的任何先前地址。您还可以选择传递一个合约数组或数组的数组,以加速多个合约的部署。此外,最后一个参数是一个可选对象,它可以包含一个单一键,即overwrite。如果overwrite设置为false,则如果已经部署了合约,则部署器将不会部署此合约。此方法返回一个 promise。

  • deployer.link(library, destinations): 将已部署的库链接到一个或多个合约。destinations参数可以是单个合约抽象或多个合约抽象的数组。如果目标中的任何合约不依赖于被链接的库,则部署器将忽略该合约。此方法返回一个 promise。

  • deployer.then(function(){}): 用于运行任意的部署步骤。在迁移期间调用特定合约函数以添加、编辑和重新组织合约数据时使用它。在回调函数内部,您将使用合约抽象 API 部署和链接合约。

可以根据要部署到的网络条件性地运行部署步骤。为了有条件地分阶段部署步骤,编写迁移以便它们接受名为network的第二个参数。一个示例用例可以是,许多流行的库已经部署到了主网络;因此,当使用这些网络时,我们不会再次部署库,而只会链接它们。以下是一个代码示例:

module.exports = function(deployer, network) { 
  if (network != "live") { 
   // Perform a different step otherwise. 
  } else { 
    // Do something specific to the network named "live". 
  } 
}

在项目中,您会发现两个迁移文件,即1_initial_migration.js2_deploy_contracts.js。第一个文件不应该被编辑,除非您知道自己在做什么。您可以自由处理另一个文件。以下是2_deploy_contracts.js文件的代码:

var ConvertLib = artifacts.require("./ConvertLib.sol"); 
var MetaCoin = artifacts.require("./MetaCoin.sol"); 

module.exports = function(deployer) { 
  deployer.deploy(ConvertLib); 
  deployer.link(ConvertLib, MetaCoin); 
  deployer.deploy(MetaCoin); 
};

这里,我们首先为CovertLib库和MetaCoin合约创建了抽象。无论使用哪个网络,我们都会部署ConvertLib库,然后将库链接到MetaCoin网络,最后部署MetaCoin网络。

要运行迁移,也就是部署合约,请运行以下命令:

truffle migrate --network development

在这里,我们告诉 truffle 在开发网络上运行迁移。如果我们不提供--network选项,则默认使用名称为development的网络。

在运行前述命令后,您会注意到 truffle 将自动更新 artifacts 文件中的ConvertLib库和MetaCoin合约地址,并更新链接。

以下是你可以提供给 migrate 子命令的一些其他重要选项:

  • --reset:从头开始运行所有迁移,而不是从上次完成的迁移开始运行。

  • -f number:从特定迁移运行合同。

你可以随时使用 truffle networks 命令在各种网络中找到项目的合同和库的地址。

单元测试合同

单元测试是测试应用程序的一种类型。这是一个过程,其中应用程序的最小可测试部分(称为单元)被单独和独立地检查是否正确运行。单元测试可以手动进行,但通常是自动化的。

Truffle 默认提供了一个单元测试框架来自动化测试你的合同。它在运行测试文件时提供了一个干净的环境;也就是说,truffle 会在每个测试文件开始时重新运行所有迁移,以确保你有一套新鲜的合同进行测试。

Truffle 让你以两种不同的方式编写简单易管理的测试:

  • 在 JavaScript 中,从应用客户端执行你的合同

  • 在 Solidity 中,从其他合同执行你的合同

两种测试方式都有各自的优点和缺点。我们将学习两种编写测试的方式。

所有测试文件都应位于 ./test 目录中。Truffle 只会运行具有以下文件扩展名的测试文件:.js.es.es6.jsx.sol。所有其他文件都会被忽略。

在运行自动化测试时,ethereumjs-testrpc 比其他客户端快得多。此外,testrpc 包含 truffle 利用的特殊功能,可将测试运行时间加速近 90%。作为一般的工作流程,我们建议你在正常开发和测试期间使用 testrpc,然后在准备部署到实时或生产网络时,再对 go-ethereum 或其他官方以太坊客户端运行一次测试。

使用 JavaScript 编写测试

Truffle 的 JavaScript 测试框架是基于 mocha 构建的。Mocha 是一个用于编写测试的 JavaScript 框架,而 chai 是一个断言库。

测试框架用于组织和执行测试,而断言库提供了验证事物是否正确的工具。断言库使得测试代码变得更容易,这样你就不必执行成千上万的 if 语句。大多数测试框架不包含断言库,并允许用户插入他们想要使用的库。

在继续之前,你需要学习如何使用 mocha 和 chai 编写测试。要学习 mocha,请访问 mochajs.org/,要学习 chai,请访问 chaijs.com/

你的测试应该存在于 ./test 目录中,并且它们应该以 .js 扩展名结尾。

合约抽象是从 JavaScript 实现合约交互的基础。因为 truffle 无法检测到你在测试中需要与哪些合约交互,所以你需要显式地请求这些合约。你可以通过使用artifacts.require()方法来实现。因此,在测试文件中应该首先为你想要测试的合约创建抽象。

然后,应该编写实际的测试。结构上,你的测试应该与 mocha 的大部分测试保持基本一致。测试文件应该包含 mocha 可识别为自动化测试的代码。使 truffle 测试与 mocha 不同的是contract()函数:此函数的工作方式与describe()完全相同,只是它会告诉 truffle 运行所有迁移。contract()函数的工作方式如下:

  • 在运行每个contract()函数之前,你的合约都会被重新部署到运行中的以太坊节点上,因此其中的测试会以清洁的合约状态运行。

  • contract()函数提供了由你的以太坊节点提供的一系列帐户,你可以用它们来编写测试。

由于 truffle 在内部使用 mocha,所以当 truffle 功能不必要时,仍然可以使用describe()来运行普通的 mocha 测试。

下面是 truffle 生成的用于测试MetaCoin合约的默认测试代码。你可以在metacoin.js文件中找到这段代码:

// Specifically request an abstraction for MetaCoin.sol 
var MetaCoin = artifacts.require("./MetaCoin.sol"); 

contract('MetaCoin', function(accounts) { 
  it("should put 10000 MetaCoin in the first account", function() { 
    return MetaCoin.deployed().then(function(instance) { 
      return instance.getBalance.call(accounts[0]); 
    }).then(function(balance) { 
      assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); 
    }); 
  }); 
  it("should send coin correctly", function() { 
    var meta; 

    // Get initial balances of first and second account. 
    var account_one = accounts[0]; 
    var account_two = accounts[1]; 

    var account_one_starting_balance; 
    var account_two_starting_balance; 
    var account_one_ending_balance; 
    var account_two_ending_balance; 

    var amount = 10; 

    return MetaCoin.deployed().then(function(instance) { 
      meta = instance; 
      return meta.getBalance.call(account_one); 
    }).then(function(balance) { 
      account_one_starting_balance = balance.toNumber(); 
      return meta.getBalance.call(account_two); 
    }).then(function(balance) { 
      account_two_starting_balance = balance.toNumber(); 
      return meta.sendCoin(account_two, amount, {from: account_one}); 
    }).then(function() { 
      return meta.getBalance.call(account_one); 
    }).then(function(balance) { 
      account_one_ending_balance = balance.toNumber(); 
      return meta.getBalance.call(account_two); 
    }).then(function(balance) { 
      account_two_ending_balance = balance.toNumber(); 

      assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender"); 
      assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver"); 
    }); 
  }); 
});

在上述代码中,你可以看到所有合约的交互代码都是使用truffle-contract库编写的。这段代码很容易理解。

最后,truffle 为你提供了对 mocha 配置的访问,以便你可以更改 mocha 的行为。mocha 的配置放在truffle.js文件导出的对象的mocha属性下。例如,看一下这个:

mocha: { 
  useColors: true 
}

在 Solidity 中编写测试

Solidity 测试代码放在.sol文件中。在使用 Solidity 编写测试之前,请注意以下事项:

  • Solidity 测试不应该继承任何合约。这样可以使你的测试尽可能简洁,并完全控制你编写的合约。

  • Truffle 为你提供了默认的断言库,但你可以随时更改此库以满足你的需求。

  • 你应该能够针对任何以太坊客户端运行你的 Solidity 测试。

要学习如何在 Solidity 中编写测试,让我们来探索 truffle 生成的默认 Solidity 测试代码。这是代码,可以在TestMetacoin.sol文件中找到:

pragma Solidity ⁰.4.2; 

import "truffle/Assert.sol"; 
import "truffle/DeployedAddresses.sol"; 
import "../contracts/MetaCoin.sol"; 

contract TestMetacoin { 

  function testInitialBalanceUsingDeployedContract() { 
    MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin()); 

    uint expected = 10000; 

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 
  } 

  function testInitialBalanceWithNewMetaCoin() { 
    MetaCoin meta = new MetaCoin(); 

    uint expected = 10000; 

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 
  } 

}

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

  • 诸如Assert.equal()之类的断言函数由truffle/Assert.sol库提供。这是默认的断言库;但是,只要该库触发正确的断言事件,您就可以包含自己的断言库,以便与 truffle 的测试运行器松散集成。断言函数触发事件,这些事件被 truffle 捕获,并显示信息。这是 truffle 中 Solidity 断言库的架构。您可以在Assert.sol中找到所有可用的断言函数(github.com/ConsenSys/truffle/blob/beta/lib/testing/Assert.sol)。

  • 在导入路径中,truffle/Assert.soltruffle是包名。我们稍后将详细了解包。

  • 您部署的合约地址(即作为迁移的一部分部署的合约)可通过truffle/DeployedAddresses.sol库获得。这由 truffle 提供,并在运行每个测试套件之前重新编译和重新链接。此库提供了所有已部署合约的函数,形式为DeployedAddresses.<contract name>()。然后,这将返回一个地址,您可以使用该地址访问该合约。

  • 要使用部署的合约,您必须将合约代码导入测试套件。请注意,上述示例中的import "../contracts/MetaCoin.sol";。此导入是相对于测试合约的,测试合约存在于./test目录中,它会超出测试目录以找到MetaCoin合约。然后,它使用该合约将地址转换为MetaCoin类型。

  • 所有测试合约都必须以大写字母Test开头。这将其与测试辅助工具和项目合约(即受测试约束的合约)区分开来,让测试运行器知道哪些合约代表测试套件。

  • 像测试合约名称一样,所有测试函数都必须以小写字母test开头。每个测试函数都按照其在测试文件中的出现顺序作为单个事务执行(例如您的 JavaScript 测试)。由truffle/Assert.sol提供的断言函数触发测试运行器评估以确定测试结果的事件。断言函数返回一个表示断言结果的布尔值,您可以使用它来提前返回测试以防止执行错误(即testrpc将暴露的错误)。

  • 你提供了许多测试钩子,如下例所示。这些钩子是beforeAllbeforeEachafterAllafterEach,与你的 JavaScript 测试中 mocha 提供的相同钩子。你可以使用这些钩子在每个测试之前和之后或在每个套件运行之前和之后执行设置和拆卸操作。像测试函数一样,每个钩子都作为单个事务执行。注意,一些复杂的测试需要执行大量设置可能会超出单个事务的燃气限制;你可以通过创建带有不同后缀的多个钩子来避免这种限制,如下例所示:

import "truffle/Assert.sol"; 

contract TestHooks { 
  uint someValue; 

  function beforeEach() { 
    someValue = 5; 
  } 

  function beforeEachAgain() { 
    someValue += 1; 
  } 

  function testSomeValueIsSix() { 
    uint expected = 6; 

    Assert.equal(someValue, expected, "someValue should have been 6"); 
  } 
}

  • 这个测试合约还显示了你的test函数和hook函数都共享相同的合约状态。你可以在测试之前设置合约数据,在测试期间使用该数据,并在准备下一个测试之前重置它。注意,就像你的 JavaScript 测试一样,你的下一个测试函数将从上一个运行的测试函数的状态继续。

Truffle 没有直接的方法来测试你的合约是否应该抛出异常(也就是说,对于使用 throw 来表示预期错误的合约)。但是有一个笨拙的解决方案,你可以在这里找到:truffleframework.com/tutorials/testing-for-throws-in-Solidity-tests

如何向测试合约发送以太币

要向你的 Solidity 测试合约发送以太币,它应该有一个名为initialBalance的返回uint的公共函数。这可以直接写成一个函数或一个公共变量。当你的测试合约部署到网络上时,truffle 会从你的测试账户向你的测试合约发送那个金额的以太币。然后,你的测试合约可以使用那个以太币在你的测试合约中脚本以太交互。注意,initialBalance是可选的,不是必需的。例如,看看下面的代码:

import "truffle/Assert.sol"; 
import "truffle/DeployedAddresses.sol"; 
import "../contracts/MyContract.sol"; 

contract TestContract { 
  // Truffle will send the TestContract one Ether after deploying the contract. 
  public uint initialBalance = 1 ether; 

  function testInitialBalanceUsingDeployedContract() { 
    MyContract myContract = MyContract(DeployedAddresses.MyContract()); 

    // perform an action which sends value to myContract, then assert. 
    myContract.send(...); 
  } 

  function () { 
    // This will NOT be executed when Ether is sent. o/ 
  } 
}

Truffle 以一种不执行回退函数的方式向你的测试合约发送以太币,所以你仍然可以在你的 Solidity 测试中使用回退函数进行高级测试用例。

运行测试

要运行你的测试脚本,只需运行这个命令:

truffle test

或者,你可以指定要运行的特定文件的路径。例如,看看这个:

truffle test ./path/to/test/file.js

包管理

一个 truffle 包是智能合约和它们的构件的集合。一个包可以依赖于零个或多个包,也就是说,你可以使用包的智能合约和构件。当在你自己的项目中使用包时,重要的是要注意有两个地方你将使用包的合约和构件:在你的项目的合约中和在你的项目的 JavaScript 代码中(迁移和测试)。

使用 truffle 创建的项目默认具有特定的布局,使它们可以被用作包。truffle包中最重要的目录如下:

  • /contracts

  • /build/contracts(由 truffle 创建)

第一个目录是你的合约目录,包括原始的 Solidity 合约。第二个目录是/build/contracts目录,其中保存着以.json文件形式的构建产物。

Truffle 支持两种包构建方式:npmethpm包。你必须知道什么是npm包,但让我们来看看什么是ethpm包。Ethpm是以太坊的包注册表。你可以在www.ethpm.com/找到所有的ethpm包。它遵循 ERC190 (github.com/ethereum/EIPs/issues/190) 规范来发布和消费智能合约包。

通过 NPM 进行包管理

Truffle 默认带有 npm 集成,并且知道项目中的node_modules目录(如果存在)。这意味着你可以通过 npm 使用和分发合约或库,使你的代码对其他人可用,其他人的代码对你可用。你的项目中也可以有一个package.json文件。你可以在项目中简单地安装任何npm包,并在任何 JavaScript 文件中导入它,但只有包含前面提到的两个目录的情况下,它才会被称为 truffle 包。在 truffle 项目中安装npm包与在任何Node.js应用程序中安装npm包相同。

通过 EthPM 进行包管理

当安装 EthPM 包时,如果不存在,将创建一个installed_contracts目录。该目录可以类似于node_modules目录进行处理。

通过 EthPM 安装包几乎与通过 NPM 安装包一样简单。你只需运行以下命令:

truffle install <package name>

您还可以安装特定版本的包:

truffle install <package name>@<version>

像 NPM 一样,EthPM 版本遵循语义版本规范。你的项目也可以定义一个ethpm.json文件,它类似于 npm 包的package.json。要安装ethpm.json文件中列出的所有依赖项,请运行以下命令:

truffle install

一个示例的ethpm.json文件如下所示:

{ 
  "package_name": "adder", 
  "version": "0.0.3", 
  "description": "Simple contract to add two numbers", 
  "authors": [ 
    "Tim Coulter <tim.coulter@consensys.net>" 
  ], 
  "keywords": [ 
    "ethereum", 
    "addition" 
  ], 
  "dependencies": { 
    "owned": "⁰.0.1" 
  }, 
  "license": "MIT" 
}

创建和发布一个 truffle 的npm包与创建任何其他npm包的过程相同。要了解如何创建和发布一个ethpm包,请访问truffleframework.com/docs/getting_started/packages-ethpm#publishing-your-own-package。无论您将包发布为npm包还是ethpm包,都需要运行truffle networks --clean命令。运行此命令时,它会删除配置文件中仅匹配*通配符的所有网络 ID 的构件。这是因为这些地址对于消费此包的其他项目来说是无效的,因为这些网络很可能是私有的,因为它们仅用于开发目的。除非你知道你在做什么,否则不应省略此命令。它将无法删除作为常量列出的私有网络的任何构件,因此您需要手动删除它们。

在你的合同内使用包的合同

要在您的合同内使用包的合同,只需像 Solidity 的import语句一样简单。当您的import路径不是明确相对或绝对时,它表示您正在寻找特定命名包中的文件。考虑使用example-truffle-librarygithub.com/ConsenSys/example-truffle-library)的示例:

import "example-truffle-library/contracts/SimpleNameRegistry.sol";

由于路径没有以./开头,truffle 知道要在您的项目的node_modulesinstalled_contracts目录中查找example-truffle-library文件夹。从那里,它解析路径以提供您请求的合同。

在 JavaScript 代码中使用包的构件

要在 JavaScript 代码中与包的构件进行交互,您只需要求该包的.json文件,然后使用truffle-contract将它们转换为可用的抽象:

var contract = require("truffle-contract"); 
var data = require("example-truffle-library/build/contracts/SimpleNameRegistry.json"); 
var SimpleNameRegistry = contract(data);

访问 Solidity 中包的合同部署地址

有时候,你可能希望你的合同与包先前部署的合同进行交互。由于部署的地址存在于包的.json文件中,Solidity 代码不能直接读取这些文件的内容。因此,使 Solidity 代码访问.json文件中的地址的流程是通过在 Solidity 代码中定义函数来设置依赖合同地址,并在合同部署后使用 JavaScript 调用这些函数来设置依赖合同地址。

所以你可以像这样定义你的合同代码:

import "example-truffle-library/contracts/SimpleNameRegistry.sol"; 

contract MyContract { 
  SimpleNameRegistry registry; 
  address public owner; 

  function MyContract { 
    owner = msg.sender; 
  } 

  // Simple example that uses the deployed registry from the package. 
  function getModule(bytes32 name) returns (address) { 
    return registry.names(name); 
  } 

  // Set the registry if you're the owner. 
  function setRegistry(address addr) { 
    if (msg.sender != owner) throw; 

    registry = SimpleNameRegistry(addr); 
  } 
}

这是您的迁移应该看起来像的:

var SimpleNameRegistry = artifacts.require("example-truffle-library/contracts/SimpleNameRegistry.sol"); 

module.exports = function(deployer) { 
  // Deploy our contract, then set the address of the registry. 
  deployer.deploy(MyContract).then(function() { 
    return MyContract.deployed(); 
  }).then(function(deployed) { 
    return deployed.setRegistry(SimpleNameRegistry.address); 
  }); 
};

使用 truffle 的控制台

有时,为了测试和调试目的或手动执行交易,与您的合同交互是很好的。Truffle 为您提供了通过交互式控制台轻松执行此操作的方式,您的合同可用并且可以立即使用。

要打开控制台,请运行此命令:

truffle console

控制台基于您的项目配置连接到以太坊节点。前述命令还接受--network选项,以指定要连接的特定节点。

以下是控制台的特点:

  • 您可以在控制台中运行该命令。例如,您可以在控制台中键入migrate --reset,它将被解释为在控制台外部运行truffle migrate --reset的方式一样。

  • 所有编译的合约都可供使用并准备就绪。

  • 在每个命令(如migrate --reset)之后,您的合约都将重新提供,因此您可以立即开始使用新分配的地址和二进制文件。

  • web3对象已经可用,并设置为连接到您的以太坊节点。

  • 所有返回 Promise 的命令都将自动解析并打印结果,无需对简单命令使用.then()。例如,您可以编写如下代码:MyContract.at("0xabcd...").getValue.call();

在 truffle 的上下文中运行外部脚本

通常,您可能希望运行与您的合约交互的外部脚本。Truffle 提供了一种简单的方法来实现这一点,根据您所需的网络引导您的合约,并根据项目配置自动连接到您的以太坊节点。

要运行外部脚本,请运行此命令:

truffle exec <path/to/file.js>

为了正确运行外部脚本,truffle 期望它们导出一个接受单个参数作为回调函数的函数。只要在此脚本中调用回调函数,您可以随意执行任何操作。回调函数接受一个错误作为其唯一参数。如果提供了错误,执行将停止,并且进程将返回一个非零退出代码。

外部脚本必须遵循以下结构:

module.exports = function(callback) { 
  // perform actions 
  callback(); 
}

Truffle 的构建流水线

现在您已经知道如何使用 truffle 编译、部署和测试智能合约,是时候为我们的代币构建一个客户端了。在介绍如何使用 truffle 构建客户端之前,您需要知道它不允许我们使用存储在以太坊节点外部的账户进行交易签名;也就是说,它没有类似于sendRawTransaction的东西,原因与truffle-contract相同。

使用 truffle 构建客户端首先意味着在客户端源代码中集成 truffle 的构件,然后准备客户端源代码以进行部署。

要构建客户端,您需要运行此命令:

truffle build

当运行此命令时,truffle 将通过检查项目配置文件中的build属性来确定如何构建客户端。

运行外部命令

可以使用命令行工具构建客户端。当build属性为字符串时,truffle 会假定我们要运行一个命令来构建客户端,因此会将字符串作为命令运行。该命令会获得充足的环境变量以与 truffle 集成。

您可以通过类似的配置代码让 truffle 运行命令行工具来构建客户端:

module.exports = { 
  // This will run the &grave;webpack&grave; command on each build. 
  // 
  // The following environment variables will be set when running the command: 
  // WORKING_DIRECTORY: root location of the project 
  // BUILD_DESTINATION_DIRECTORY: expected destination of built assets 
  // BUILD_CONTRACTS_DIRECTORY: root location of your build contract files (.sol.js) 
  // 
  build: "webpack" 
}

运行自定义函数

JavaScript 函数可用于构建客户端。当 build 属性是一个函数时,松露将在我们想要构建客户端时运行该函数。该函数提供了有关项目如何与松露集成的大量信息。

你可以让松露运行一个函数来构建客户端,使用类似的配置代码:

module.exports = { 
  build: function(options, callback) { 
     // Do something when a build is required. &grave;options&grave; contains these values: 
     // 
     // working_directory: root location of the project 
     // contracts_directory: root directory of .sol files 
     // destination_directory: directory where truffle expects the built assets (important for &grave;truffle serve&grave;) 
  } 
}

你也可以创建一个对象,其中包含一个像这里的 build 方法。这对于想要发布一个构建客户端的包的人来说是很好的。

松露的默认构建器

松露提供了 truffle-default-builder npm 包,被称为松露的默认构建器。该构建器导出一个对象,其中有一个与之前提到的方法完全相同的构建方法。

默认构建器可用于为你的 DApp 构建 web 客户端,其服务器仅提供静态文件,并且所有功能都在前端。

在我们深入了解如何使用默认构建器之前,首先使用以下命令安装它:

npm install truffle-default-builder --save

现在将你的配置文件更改为这样:

var DefaultBuilder = require("truffle-default-builder"); 

module.exports = { 
  networks: { 
    development: { 
      host: "localhost", 
      port: 8545, 
      network_id: "10" 
    }, 
    live: { 
         host: "localhost", 
      port: 8545, 
      network_id: "1" 
    } 
  }, 
  build: new DefaultBuilder({ 
    "index.html": "index.html", 
    "app.js": [ 
      "javascripts/index.js" 
    ], 
    "bootstrap.min.css": "stylesheets/bootstrap.min.css" 
  }) 
};

默认构建器让你完全控制你想要组织客户端文件和文件夹的方式。

这个配置描述了 targets(左侧)的文件、文件夹和构成 targets 内容的文件数组(右侧)。每个目标将通过处理右侧的文件根据它们的文件扩展名进行生成,将结果连接在一起,然后将结果文件(目标)保存到构建目标中。在这里,右侧指定了一个字符串而不是一个数组,如果需要,该文件将被处理,然后直接复制过去。如果字符串以 "/" 结尾,它将被解释为一个目录,目录将被直接复制过去而不进行进一步处理。右侧指定的所有路径都是相对于 app/ 目录的。

你可以随时更改此配置和目录结构。例如,你不需要 javascriptsstylesheets 目录,但请确保你相应地编辑你的配置。

如果你希望默认的构建器将松露集成到你的 web 应用程序的前端,请确保你有一个名为 app.js 的构建目标,该默认构建器可以将代码追加到其中。它不会将松露与任何其他文件名集成。

这里是默认构建器的特点:

  • 自动导入你的编译合同构件、部署的合同信息和以太坊节点配置到客户端源代码中

  • 包括建议的依赖项,包括 web3 和 truffle-contract

  • 编译 ES6JSX 文件

  • 编译SASS文件

  • 最小化 asset 文件

你可以使用 truffle watch 命令,它会监视 contracts 目录、app 目录和配置文件的更改。当有更改时,它会重新编译合约并生成新的构建文件,然后重新构建客户端。但它不会运行迁移和测试。

构建一个客户端

现在让我们为我们的 DApp 编写一个客户端,并使用 truffle 的默认构建器构建它。首先,根据我们设置的先前配置创建文件和目录:创建一个 app 目录,在其中创建一个 index.html 文件和两个名为 javascriptsstyelsheets 的目录。在 javascripts 目录中,创建一个名为 index.js 的文件,在 stylesheets 目录中,下载并放置 Bootstrap 4 的 CSS 文件。你可以在 v4-alpha.getbootstrap.com/getting-started/download/#bootstrap-css-and-js 找到它。

index.html 文件中,放置以下代码:

<!doctype html> 
<html> 
   <head> 
         <link rel="stylesheet" type="text/css" href="bootstrap.min.css"> 
   </head> 
   <body> 
   <div class="container"> 
         <div class="row"> 
         <div class="col-md-6"> 
         <br> 
         <h2>Send Metacoins</h2> 
         <hr> 
         <form id="sendForm"> 
         <div class="form-group"> 
               <label for="fromAddress">Select Account Address</label> 
         <select class="form-control" id="fromAddress"> 
         </select> 
         </div> 
         <div class="form-group"> 
         <label for="amount">How much metacoin do you want to send?
         </label> 
         <input type="text" class="form-control" id="amount"> 
         </div> 
         <div class="form-group"> 
         <label for="toAddress">Enter the address to which you want to 
           send matacoins</label> 
         <input type="text" class="form-control" id="toAddress"  
             placeholder="Prefixed with 0x"> 
         </div> 
         <button type="submit" class="btn btn-primary">Submit</button> 
         </form> 
         </div> 
         <div class="col-md-6"> 
         <br> 
         <h2>Find Balance</h2> 
         <hr> 
         <form id="findBalanceForm"> 
         <div class="form-group"> 
               <label for="address">Select Account Address</label> 
               <select class="form-control" id="address"> 
               </select> 
         </div> 
         <button type="submit" class="btn btn-primary">Check 
            Balance</button> 
         </form> 
         </div> 
         </div> 
         </div> 
         <script type="text/javascript" src="img/app.js"></script> 
   </body> 
</html>

<!doctype html> 
 <html> 
     <head> 
         <link rel="stylesheet" type="text/css" href="bootstrap.min.css"> 
     </head> 
     <body> 
         <div class="container"> 
             <div class="row"> 
                 <div class="col-md-6"> 
                     <br> 
                     <h2>Send Metacoins</h2> 
                     <hr> 
                     <form id="sendForm"> 
                         <div class="form-group"> 
                             <label for="fromAddress">Select Account Address</label> 
                             <select class="form-control" id="fromAddress"> 
                             </select> 
                           </div> 
                           <div class="form-group"> 
                             <label for="amount">How much metacoin you want to send?</label> 
                             <input type="text" class="form-control" id="amount"> 
                           </div> 
                           <div class="form-group"> 
                             <label for="toAddress">Enter the address to which you want to send matacoins</label> 
                             <input type="text" class="form-control" id="toAddress" placeholder="Prefixed with 0x"> 
                           </div> 
                           <button type="submit" class="btn btn-primary">Submit</button> 
                     </form> 
                 </div> 
                 <div class="col-md-6"> 
                     <br> 
                     <h2>Find Balance</h2> 
                     <hr> 
                     <form id="findBalanceForm"> 
                         <div class="form-group"> 
                             <label for="address">Select Account Address</label> 
                             <select class="form-control" id="address"> 
                             </select> 
                           </div> 
                           <button type="submit" class="btn btn-primary">Check Balance</button> 
                     </form> 
                 </div> 
             </div> 
         </div> 
         <script type="text/javascript" src="img/app.js"></script> 
     </body> 
 </html>

在上述代码中,我们加载了 bootstrap.min.cssapp.js 文件。我们有两个表单:一个用于向不同账户发送 metacoins,另一个用于检查账户的 metacoins 余额。在第一个表单中,用户必须选择一个账户,然后输入要发送的 metacoin 金额和要发送到的地址。而在第二个表单中,用户只需选择其想要检查 metacoin 余额的地址。

index.js 文件中,放置以下代码:

window.addEventListener("load", function(){ 
   var accounts = web3.eth.accounts; 

   var html = ""; 

   for(var count = 0; count < accounts.length; count++) 
   { 
         html = html + "<option>" + accounts[count] + "</option>"; 
   } 

   document.getElementById("fromAddress").innerHTML = html; 
   document.getElementById("address").innerHTML = html; 

   MetaCoin.detectNetwork(); 
}) 

document.getElementById("sendForm").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   MetaCoin.deployed().then(function(instance){ 
         return instance.sendCoin(document.getElementById("toAddress").value, document.getElementById("amount").value, { 
               from: document.getElementById("fromAddress").options[document.getElementById("fromAddress").selectedIndex].value 
         }); 
   }).then(function(result){ 
         alert("Transaction mined successfully. Txn Hash: " + result.tx); 
   }).catch(function(e){ 
         alert("An error occured"); 
   }) 
}) 

document.getElementById("findBalanceForm").addEventListener("submit", function(e){ 
   e.preventDefault(); 

   MetaCoin.deployed().then(function(instance){ 
         return instance.getBalance.call(document.getElementById("address").value); 
   }).then(function(result){ 
         console.log(result); 
         alert("Balance is: " + result.toString() + " metacoins"); 
   }).catch(function(e){ 
         alert("An error occured"); 
   }) 
})

以下是代码的工作原理:

  1. truffle-default-builder 使构件对象可在 __contracts__ 全局对象下使用。

  2. 它还通过将变量名设置为合约名,为所有可用的合约提供了合约抽象作为全局变量。

  3. 它还通过已设置提供程序为 web3 对象提供了支持。它还为合约抽象设置了提供程序。它使得 web3 对象连接到名为 development 的网络,如果不存在,则默认值为 http://localhost:8545

  4. 在上述代码中,首先,我们等待页面加载完成,一旦加载完成,我们就检索连接节点中的账户列表,并在两个表单中显示它们。我们还调用 MetaCoin 抽象的 detectNetwork() 方法。

  5. 然后,我们为两个表单都设置了 submit 事件处理程序。它们都按预期执行其操作,并在弹出窗口中显示结果。

  6. 当第一个表单提交时,我们获取 MetaCoin 合约的部署实例,并使用正确的参数调用 sendCoin 方法。

  7. 当第二个表单提交时,我们通过调用 EVM 中的 getBalance 方法而不是广播交易来检索所选账户的余额。

现在继续运行 truffle build 命令,你会注意到 truffle 将在 build 目录中创建 index.htmlapp.jsbootstrap.min.css 文件,并在其中放置客户端的最终部署代码。

Truffle 的服务器

Truffle 带有内置的 Web 服务器。这个 Web 服务器只是以适当的 MIME 类型设置提供build目录中的文件。除此之外,它没有配置为执行任何其他操作。

要运行 Web 服务器,请运行此命令:

truffle serve

默认情况下,服务器在端口号 8080 上运行。但你可以使用-p选项指定不同的端口号。

类似于 truffle watch,这个 Web 服务器还监视contracts目录、app目录和配置文件中的更改。当有更改时,它重新编译合约并生成新的工件文件,然后重建客户端。但它不会运行迁移和测试。

由于 truffle-default-builder 将最终可部署的代码放在构建目录中,你只需运行truffle serve即可通过 Web 提供文件。

让我们测试我们的 Web 客户端。访问http://localhost:8080,你会看到这个截图:

所选框中的帐户地址对你而言会有所不同。现在在部署合约时,合约将所有的 metacoins 分配给部署合约的地址;所以在这里,第一个帐户将有 10,000 个 metacoins 的余额。现在从第一个帐户发送五个 metacoins 到第二个帐户,然后点击提交。你会看到类似以下截图的屏幕:

现在通过在第二个表单的选择框中选择第二个帐户然后点击“检查余额”按钮来检查第二个帐户的余额。你会看到类似以下截图的屏幕:

摘要

在本章中,我们深入了解了如何使用 truffle 构建 DApps 及其相应的客户端。我们看到 truffle 如何简化编写、编译、部署和测试 DApps 的过程。我们还看到了使用truffle-contract在客户端之间轻松切换网络而无需触及源代码的简便之处。现在,你已经准备好开始使用 truffle 构建企业级 DApps 了。

在下一章中,我们将使用 truffle 和以太坊闹钟 DApp 构建一个分散式闹钟应用程序,该应用程序在时间上为你支付报酬。

只需用冒号":"替换i.e.

第九章:构建联盟区块链

联盟(通常由多个参与者组成,如银行、电子商务网站、政府实体、医院等)可以利用区块链技术解决许多问题,并使事情变得更快更便宜。尽管他们找出了区块链如何帮助他们,以太坊的区块链实现并不完全适合他们的所有情况。虽然有其他区块链实现(例如 Hyperledger)专门为联盟而构建,但正如我们在整本书中学到的以太坊一样,我们将看到如何使用以太坊构建联盟区块链。基本上,我们将使用 Parity 来构建联盟区块链。虽然 Parity 还有其他替代方案,例如 J.P. Morgan 的 quorum,但在撰写本书时,Parity 已经存在一段时间,许多企业已经在使用它,而其他替代方案尚未被任何企业使用。但根据您的需求,Parity 可能不是最佳解决方案;因此,在决定使用哪种解决方案之前,请先调查所有其他解决方案。

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

  • 为什么以太坊不适合联盟区块链?

  • 什么是 Parity 节点以及其特点?

  • 什么是权威证明共识协议,以及 Parity 支持哪些类型的 PoA?

  • Aura 共识协议是如何工作的?

  • 下载并安装 Parity

  • 使用 Parity 构建联盟区块链

什么是联盟区块链?

要理解什么是联盟区块链,或者换句话说,联盟需要什么样的区块链实现,让我们来看一个例子。银行希望建立一个区块链来使资金转账更加简单、快速和便宜。在这种情况下,以下是他们需要的东西:

  1. 速度:他们需要一个能够在几乎实时确认交易的区块链网络。目前,以太坊区块链网络的出块时间为 12 秒,客户通常需要等待几分钟才能确认一笔交易。

  2. 有权限:他们希望区块链是有权限的。权限本身意味着各种不同的事情。例如:权限可能包括获得加入网络的许可,可能包括获得创建区块的许可,也可能包括获得发送特定交易的许可等等。

  3. 安全性:对于私有网络来说,PoW 并不足够安全,因为参与者数量有限;因此,没有足够的算力来保证其安全性。因此,需要一种能够保持区块链安全和不可变的共识协议。

  4. 隐私性:尽管网络是私有的,但网络本身仍然需要隐私。有两种隐私形式:

  5. 身份隐私: 身份隐私是使身份无法追踪的行为。我们先前看到的获得身份隐私的解决方案是使用多个以太坊账户地址。但是,如果使用多个以太坊账户,则智能合约将无法通过所有这些账户是否实际属于同一用户的所有权验证而失败。

  6. 数据隐私: 有时,我们不希望数据对网络中的所有节点可见,而是只对特定节点可见。

总的来说,在本章中,我们将学习如何解决以太坊中的这些问题。

什么是权威证明共识?

PoA 是区块链的一种共识机制,其共识是通过参考验证者列表(当它们与实体联系在一起时称为权威)来实现的。验证者是允许参与共识的一组账户/节点;它们验证交易和区块。

与 PoW 或 PoS 不同,它不涉及挖矿机制。有各种类型的 PoA 协议,它们根据它们的实际工作方式而有所不同。Hyperledger 和 Ripple 基于 PoA。Hyperledger 基于 PBFT,而 Ripple 使用迭代过程。

Parity 简介

Parity 是一款专为正确性/可验证性、模块化、低占用空间和高性能而编写的以太坊节点。它是用 Rust 编程语言编写的,这是一种以效率为重点的混合命令式/OO/函数式语言。它是由 Parity Technologies 专业开发的。在撰写本书时,Parity 的最新版本是 1.7.0,我们将使用此版本。我们将学习构建联盟区块链所需的所有内容。要深入学习 Parity,您可以参考官方文档。

它比 go-ethereum 有更多的功能,例如 web3 dapp 浏览器,更先进的账户管理等等。但它之所以特别之处在于,它支持权威证明PoA)以及 PoW。Parity 目前支持 Aura 和 Tendermint PoA 协议。未来,它可能支持更多的 PoA 协议。当前,Parity 推荐使用 Aura 而不是 Tendermint,因为 Tendermint 仍在开发中。

Aura 对于权限区块链比 PoW 是一个更好的解决方案,因为它具有更好的区块时间,并在私有网络中提供了更好的安全性。

理解 Aura 工作原理

让我们高层次地看一下 Aura 是如何工作的。Aura 要求在每个节点中指定相同的验证者列表。这是参与共识的账户地址列表。一个节点可能是一个验证节点,也可能不是。即使是验证节点也需要拥有此列表,以便它自己能够达成共识。

如果验证者列表永远不会改变,此列表可以在创世文件中提供为静态列表;或者可以在智能合约中提供,以便可以动态更新,并且每个节点都知道它。在智能合约中,你可以配置各种关于谁可以添加新验证者的策略。

区块时间在创世文件中是可配置的。由你决定区块时间。在私有网络中,低至三秒的区块时间效果良好。在 Aura 中,每隔三秒钟选择一个验证者,该验证者负责创建、验证、签名和广播区块。我们不需要太多了解实际的选择算法,因为这不会影响我们的 dapp 开发。但这是计算下一个验证者的公式,(UNIX_TIMESTAMP / BLOCK_TIME % NUMBER_OF_TOTAL_VALIDATORS)。选择算法足够智能,给每个人平等的机会。当其他节点接收到一个区块时,它们会检查它是否来自下一个有效的验证者;如果不是,它们会拒绝它。与 PoW 不同,当一个验证者创建一个区块时,它不会获得以太币奖励。在 Aura 中,当没有交易时,是否生成空区块由我们决定。

你一定在想,如果由于某种原因,下一个验证节点无法创建和广播下一个区块会发生什么。为了理解这一点,让我们举个例子:假设 A 是下一个区块(即第五个区块)的验证者,B 是第六个区块的验证者。假设区块时间为五秒。如果 A 未能广播一个区块,那么五秒后当 B 轮到时,它将广播一个区块。所以实际上并没有发生什么严重的事情。区块时间戳将显示这些细节。

你可能也想知道是否有可能出现网络以多个不同的区块链结束的情况,就像在 PoW 中两个矿工同时挖矿的情况一样。是的,这种情况可能有很多种。让我们举一个例子,了解一种可能发生的情况以及网络如何自动解决它。假设有五个验证者:A、B、C、D 和 E。区块时间为五秒。假设首先选择 A,并且它广播了一个区块,但由于某种原因,该区块未到达 D 和 E;因此他们会认为 A 没有广播该区块。现在假设选择算法选择 B 来生成下一个区块;那么 B 将在 A 的区块之上生成下一个区块并广播给所有节点。D 和 E 将拒绝它,因为前一个区块的哈希值不匹配。因此,D 和 E 将形成不同的链,而 A、B 和 C 将形成不同的链。A、B 和 C 将拒绝来自 D 和 E 的区块,而 D 和 E 将拒绝来自 A、B 和 C 的区块。这个问题在节点之间解决,因为 A、B 和 C 持有的区块链比 D 和 E 持有的区块链更准确;因此 D 和 E 将用 A、B 和 C 持有的区块链替换他们的区块链版本。这两个版本的区块链将具有不同的准确度分数,第一个区块链的分数将比第二个区块链的分数更高。当 B 广播它的区块时,它还将提供其区块链的分数,由于其分数更高,D 和 E 将用 B 的区块链替换他们的区块链。这就是冲突是如何解决的。区块链的链分数是使用(U128_max * BLOCK_NUMBER_OF_LATEST_BLOCK - (UNIX_TIMESTAMP_OF_LATEST_BLOCK / BLOCK_TIME))来计算的。首先按长度对链进行评分(区块越多,越好)。对于长度相等的链,选择最后一个区块更老的链。

你可以深入了解 Aura 在github.com/paritytech/parity/wiki/Aura

让 parity 运行起来

Parity 需要 Rust 版本 1.16.0 来构建。推荐通过 rustup 安装 Rust。

安装 rust

如果你还没有安装 rustup,你可以像这样安装它。

Linux

在基于 Linux 的操作系统上,运行此命令:

curl https://sh.rustup.rs -sSf | sh

Parity 还需要安装gccg++libssl-dev/openssllibudev-devpkg-config软件包。

OS X

在 OS X 上,运行此命令:

curl https://sh.rustup.rs -sSf | sh

Parity 还需要 clang。Clang 随 Xcode 命令行工具一起提供,或者可以使用 Homebrew 安装。

Windows

确保你安装了带有 C++支持的 Visual Studio 2015。接下来,从static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe下载并运行 rustup 安装程序,启动"VS2015 x64 Native Tools Command Prompt",并使用以下命令安装和设置msvc工具链:

rustup default stable-x86_64-pc-windows-msvc

下载、安装和运行 parity

现在,在您的操作系统上安装了 rust 之后,您可以运行以下简单的单行命令来安装 parity:

cargo install --git https://github.com/paritytech/parity.git parity

要检查是否安装了 parity,运行以下命令:

parity --help

如果成功安装了 parity,则会看到一个子命令和选项的列表。

创建私人网络

现在是时候设置我们的联合区块链了。我们将使用 Aura 来创建两个互相连接的验证节点进行共识。我们会在同一台计算机上设置它们。

创建账户

首先,打开两个 shell 窗口。其中一个是给第一个验证者,另一个是给第二个验证者。第一个节点将包含两个账户,第二个节点将包含一个账户。第一个节点的第二个账户将被分配一些初始 ether,这样网络就会有一些 ether。

在第一个 shell 中,运行以下命令两次:

parity account new -d ./validator0 

两次都会要求您输入密码。现在只需为两个账户设置相同的密码。

在第二个 shell 中,只运行一次以下命令:

parity account new  -d ./validator1 

就像以前一样,输入密码。

创建一个规范文件

每个网络的节点共享一个通用的规范文件。该文件告诉节点关于创世块,谁是验证者等信息。我们将创建一个智能合约,其中将包含验证者列表。有两种类型的验证器合约:非报告合约和报告合约。我们只需要提供一个。

区别在于非报告合同只返回验证器列表,而报告合同可以对良性(良性的不端行为可能只是未从指定验证者那里接收到一个块)和恶意不端行为(恶意的不端行为是发布了两个不同的块来同一个步骤)采取行动。

非报告合同至少应该具有以下接口:

{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}

getValidators 函数将在每个块上被调用以确定当前列表。然后,切换规则由实施该方法的合同确定。

报告合同至少应该具有以下接口:

[ 
    {"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}, 
    {"constant":false,"inputs":[{"name":"validator","type":"address"}],"name":"reportMalicious","outputs":[],"payable":false,"type":"function"}, 
    {"constant":false,"inputs":[{"name":"validator","type":"address"}],"name":"reportBenign","outputs":[],"payable":false,"type":"function"} 
]

当存在良性或恶意行为时,共识引擎分别调用 reportBenignreportMalicious 函数。

让我们创建一个报告合同。下面是一个基本示例:

contract ReportingContract { 
   address[] public validators = [0x831647ec69be4ca44ea4bd1b9909debfbaaef55c, 0x12a6bda0d5f58538167b2efce5519e316863f9fd]; 
   mapping(address => uint) indices; 
   address public disliked; 

   function ReportingContract() { 
       for (uint i = 0; i < validators.length; i++) { 
           indices[validators[i]] = i; 
       } 
   } 

   // Called on every block to update node validator list. 
    function getValidators() constant returns (address[]) { 
      return validators; 
   } 

   // Expand the list of validators. 
   function addValidator(address validator) { 
      validators.push(validator); 
   } 

   // Remove a validator from the list. 
   function reportMalicious(address validator) { 
      validators[indices[validator]] = validators[validators.length-1]; 
      delete indices[validator]; 
      delete validators[validators.length-1]; 
      validators.length--; 
   } 

   function reportBenign(address validator) { 
       disliked = validator; 
   } 
}

这段代码是不言而喻的。确保在验证器数组中用第一个验证者 1 和验证者 2 节点的第一个地址替换地址,因为我们将使用这些地址进行验证。现在使用您感到舒适的任何方法编译前述合同。

现在我们来创建规范文件。创建一个名为 spec.json 的文件,并将以下代码放入其中:

{ 
    "name": "ethereum", 
    "engine": { 
        "authorityRound": { 
            "params": { 
               "gasLimitBoundDivisor": "0x400", 
               "stepDuration": "5", 
               "validators" : { 
               "contract": "0x0000000000000000000000000000000000000005" 
                } 
            } 
        } 
    }, 
    "params": { 
        "maximumExtraDataSize": "0x20", 
        "minGasLimit": "0x1388", 
        "networkID" : "0x2323" 
    }, 
    "genesis": { 
      "seal": { 
       "authorityRound": { 
         "step": "0x0", 
          "signature": "0x00000000000000000000000000000000000000000
             000000000000000000000000000000000000000000000000000000
                00000000000000000000000000000000000" 
            } 
        }, 
        "difficulty": "0x20000", 
        "gasLimit": "0x5B8D80" 
    }, 
    "accounts": { 
        "0x0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, 
        "0x0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, 
        "0x0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, 
        "0x0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, 
        "0x0000000000000000000000000000000000000005": { "balance": "1", "constructor" : "0x606060405260406040519081016040528073831647" }, 
        "0x004ec07d2329997267Ec62b4166639513386F32E": { "balance": "10000000000000000000000" } 
    } 
}

下面是之前文件的工作原理:

  • engine 属性用于设置共识协议和协议特定参数。在这里,引擎是 authorityRound,即 aura。gasLimitBoundDivisor 确定了 gas 限制调整,具有通常的 ethereum 值。在 validators 属性中,我们有一个 contract 属性,这是报告合约的地址。stepDuration 是以秒为单位的区块时间。

  • params 属性中,只有网络 ID 是重要的;其他的对所有链都是标准的。

  • genesis 对于 authorityRound 共识有一些标准值。

  • accounts 用于列出网络中存在的初始帐户和合约。前四个是标准的以太坊内置合约;这些应该包括以使用 Solidity 合约编写语言。第五个是报告合约。确保将字节码替换为您的字节码,在 constructor 参数中使用。最后一个帐户是验证器 1 shell 中生成的第二个帐户。它用于向网络提供以太。将此地址替换为您自己的地址。

在我们继续之前,创建另一个名为 node.pwds 的文件。在该文件中,放置您创建的帐户的密码。这个文件将被验证者用来解锁帐户以签署区块。

启动节点

现在我们已经准备好启动我们的验证节点所需的所有基本要求。在第一个 shell 中,运行以下命令来启动第一个验证节点:

parity  --chain spec.json -d ./validator0 --force-sealing --engine-signer "0x831647ec69be4ca44ea4bd1b9909debfbaaef55c" --port 30300 --jsonrpc-port 8540 --ui-port 8180 --dapps-port 8080 --ws-port 8546 --jsonrpc-apis web3,eth,net,personal,parity,parity_set,traces,rpc,parity_accounts --password "node.pwds"

以下是前述命令的工作原理:

  • --chain 用于指定规范文件的路径。

  • -d 用于指定数据目录。

  • --force-sealing 确保即使没有交易,也会产生区块。

  • --engine-signer 用于指定节点将用于签名区块的地址,即验证者的地址。如果存在恶意权限,则建议使用 --force-sealing;这将确保正确的链是最长的。确保将地址更改为您生成的地址,即在此 shell 中生成的第一个地址。

  • --password 用于指定密码文件。

在第二个 shell 中,运行以下命令来启动第二个验证节点:

parity  --chain spec.json -d ./validator1 --force-sealing --engine-signer "0x12a6bda0d5f58538167b2efce5519e316863f9fd" --port 30301 --jsonrpc-port 8541 --ui-port 8181 --dapps-port 8081 --ws-port 8547 --jsonrpc-apis web3,eth,net,personal,parity,parity_set,traces,rpc,parity_accounts --password "/Users/narayanprusty/Desktop/node.pwds" 

在这里,请确保将地址更改为您在此 shell 中生成的地址。

连接节点

最后,我们需要连接这两个节点。打开一个新的 shell 窗口,并运行以下命令来查找连接到第二个节点的 URL:

curl --data '{"jsonrpc":"2.0","method":"parity_enode","params":[],"id":0}' -H "Content-Type: application/json" -X POST localhost:8541

您将会得到这样的输出:

{"jsonrpc":"2.0","result":"enode://7bac3c8cf914903904a408ecd71635966331990c5c9f7c7a291b531d5912ac3b52e8b174994b93cab1bf14118c2f24a16f75c49e83b93e0864eb099996ec1af9@[::0.0.1.0]:30301","id":0}

现在运行以下命令,将 enode URL 和 IP 地址中的编码 URL 替换为 127.0.0.1:

curl --data '{"jsonrpc":"2.0","method":"parity_addReservedPeer","params":["enode://7ba..."],"id":0}' -H "Content-Type: application/json" -X POST localhost:8540

您应该得到这个输出:

{"jsonrpc":"2.0","result":true,"id":0}

节点应该在控制台中指示 0/1/25 对等节点,这意味着它们彼此连接。这是一个参考图像:

许可和隐私

我们看到了 parity 如何解决速度和安全性的问题。Parity 目前没有提供任何特定于许可和隐私的内容。让我们看看如何在 parity 中实现这一点:

  1. 权限管理:Parity 网络可以通过配置每个节点的服务器,仅允许特定 IP 地址的连接,来实现权限管理,决定谁可以加入,谁不可以加入。即使 IP 地址没有被阻止,要连接网络中的一个节点,新节点也需要一个我们之前看到的 enode 地址,而这个地址是无法猜测的。因此,默认情况下存在基本的保护。但是没有强制执行这一点。网络中的每个节点都必须自行处理此问题。类似的权限管理,可以通过智能合约来确定谁可以创建区块,谁不可以创建。最后,节点可以发送什么样的交易目前是不可配置的。

  2. 身份隐私:通过仍然启用所有权检查的技术,可以实现身份隐私。在设置所有权时,所有者需要指定一个不确定的非对称加密的公钥。每当它想要通过所有权检查时,它将提供常见文本的加密形式,合约将解密它并查看账户是否为所有者。合约应确保相同的加密数据不会被检查两次。

  3. 数据隐私:如果您只是使用区块链来存储数据,可以使用对称加密来加密数据,并与您想要查看数据的人共享密钥。但无法对加密数据进行操作。如果需要对输入数据进行操作并仍然保护隐私,则各方必须完全建立一个不同的区块链网络。

概要

总的来说,在本章中,我们学习了如何使用 Parity 以及 aura 的工作原理,以及在 Parity 中实现权限管理和隐私的一些技巧。现在,您至少应该有足够的信心为使用区块链的联盟构建一个概念验证。现在,您可以继续探索其他解决方案,例如 Hyperledger 1.0 和 Quorum,用于构建联盟区块链。目前,以太坊正式致力于使其更适合联盟;因此,请密切关注各种区块链信息来源,了解市场上的新动态。

posted @ 2024-05-01 15:26  绝不原创的飞龙  阅读(81)  评论(0编辑  收藏  举报