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

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

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

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

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

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

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

  • uint8uint16uint24uint256 用于分别存储 8 位、16 位、24 位 … 256 位的无符号整数。同样,int8int16int256 分别用于存储 8 位、16 位 … 256 位的有符号整数。uintintuint256int256 的别名。类似于 uintintufixedfixed 用于表示小数。ufixed0x8ufixed0x16ufixed0x256 用于分别存储 8 位、16 位 … 256 位的无符号小数。同样,fixed0x8fixed0x16fixed0x256 用于分别存储 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 库来实现这一目标。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值