区块链项目构建指南(一)
原文:
zh.annas-archive.org/md5/e61d4f5cf7a1ecdfea6a6e32a165bf64译者:飞龙
序言
区块链是一个去中心化的账本,它维护着一个持续增长的数据记录列表,受到篡改和修订的保护。每个用户都可以连接到网络,向其发送新交易,验证交易,并创建新的区块。
本书将教会您什么是区块链,它如何维护数据完整性,以及如何使用以太坊创建真实世界的区块链项目。通过有趣的真实世界项目,您将学会如何编写智能合约,这些合约完全按照程序编写,没有欺诈、审查或第三方干预的机会,并构建端到端的区块链应用程序。您将学习加密货币中的密码学概念、以太安全、挖矿、智能合约和 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 并注册,文件将直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册到我们的网站。
-
将鼠标指针悬停在顶部的 SUPPORT 标签上。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名。
-
选择要下载代码文件的书籍。
-
从下拉菜单中选择您从何处购买了本书。
-
单击“代码下载”。
下载文件后,请确保使用最新版本的解压缩或提取文件夹:
-
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 协议),或者使用PBFT(Practical 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 节点和客户端都同意这个命名约定。如果我们尝试在 d 和 ds 命名空间中存储无效的键,那么客户端将过滤掉无效的键。
支持 .bit 域的浏览器需要在 Namecoin 的 d 和 ds 命名空间中查找与 .bit 域关联的 IP 地址。
d 和 ds 命名空间的区别在于 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 的十六进制字符串。
每个账户由一个地址表示。一旦我们有了生成地址所需的密钥,这里是从公钥生成地址的步骤:
-
首先,生成公钥的
keccak-256哈希。这将给你一个 256 位的数字。 -
放弃前 96 位,也就是 12 个字节。你现在应该有 160 个位的二进制数据,也就是 20 个字节。
-
现在将地址编码为十六进制字符串。因此最终,你将得到一个由 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 中,变量根据上下文的不同,会被存储在内存和文件系统中。
根据上下文的不同,总是存在一个默认的位置。但是对于字符串、数组和结构等复杂数据类型,可以通过在类型后添加storage或memory来覆盖默认位置。函数参数(包括返回参数)的默认位置是内存,局部变量的默认位置是存储,当然状态变量的位置强制为存储。
数据位置很重要,因为它们会改变赋值的行为:
-
存储变量与内存变量之间的赋值总是创建独立的副本。但是,从一个存储在内存中的复杂类型赋值给另一个存储在内存中的复杂类型并不会创建副本。
-
对状态变量的赋值(即使来自其他状态变量)总是创建独立的副本。
-
你不能将存储在内存中的复杂类型赋值给本地存储变量。
-
在将状态变量赋值给本地存储变量时,本地存储变量指向状态变量;也就是说,本地存储变量成为了指针。
有哪些不同的数据类型?
Solidity 是一种静态类型语言;变量所持有的数据类型需要预先定义。默认情况下,所有变量的位都被赋值为 0。在 Solidity 中,变量是函数作用域的;也就是说,在函数内部声明的任何变量都将对整个函数的作用域有效,无论它在何处声明。
现在让我们来看看 Solidity 提供的各种数据类型:
-
最简单的数据类型是
bool。它可以存储true或false。 -
uint8、uint16、uint24...uint256用于分别存储 8 位、16 位、24 位 ... 256 位的无符号整数。同样,int8、int16...int256分别用于存储 8 位、16 位 ... 256 位的有符号整数。uint和int是uint256和int256的别名。类似于uint和int,ufixed和fixed用于表示小数。ufixed0x8、ufixed0x16...ufixed0x256用于分别存储 8 位、16 位 ... 256 位的无符号小数。同样,fixed0x8、fixed0x16...fixed0x256用于分别存储 8 位、16 位 ... 256 位有符号小数。如果一个数字需要多于 256 位的存储空间,那么就使用 256 位的数据类型,于此情况下将存储该数字的近似值。 -
address用于通过分配十六进制文字来存储最多 20 个字节的值。它用于存储以太坊地址。address类型公开两个属性:balance和send。balance用于检查地址的余额,send用于向地址转移以太币。send 方法接受需要转移的 wei 数量,并根据转移是否成功返回 true 或 false。wei 从调用send方法的合同中扣除。你可以在 Solidity 中使用0x前缀为变量分配十六进制编码的值。
数组
Solidity 支持通用数组和字节数组。它支持固定大小和动态数组。它还支持多维数组。
bytes1、bytes2、bytes3、...、bytes32 是字节数组的类型。byte 是 bytes1 的别名。
这里是展示通用数组语法的示例:
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 中,有两种创建字符串的方法:使用 bytes 和 string。bytes 用于创建原始字符串,而 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可以转换为uint16,int128可以转换为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 支持if、else、while、for、break、continue、return、? :控制结构。
下面是一个演示控制结构的例子:
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关键字代表当前合约实例。
合约的特性
现在是时候深入了解合约了。我们将看一些新功能,并且深入了解我们已经看过的功能。
可见性
状态变量或函数的可见性定义了谁可以看到它。函数和状态变量有四种可见性:external、public、internal和private。
默认情况下,函数的可见性是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合约的最终继承链是sample6、sample5、sample4、sample2、sample3、sample1。继承链从最派生的合约开始,以最少派生的合约结束。
抽象合约
只包含函数原型而非实现的合约称为抽象合约。这样的合约不能被编译(即使它们包含了实现的函数和未实现的函数)。如果一个合约继承自一个抽象合约并且没有通过覆盖实现所有未实现的函数,那它本身就是抽象的。
这些抽象合约只是用来让编译器知道接口。当你引用已部署合约并调用它的函数时,这是有用的。
下面是一个用来示范这一点的例子:
contract sample1
{
function a() returns (int b);
}
contract sample2
{
function myFunc()
{
sample1 s = sample1(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
//without abstract contract this wouldn't have compiled
s.a();
}
}
库
库与合约类似,但它们的目的是在特定地址只部署一次,并且它们的代码被各种合约重复使用。这意味着如果库函数被调用,它们的代码将在调用合约的上下文中执行;也就是说,this指向调用合约,特别是可以访问来自调用合约的存储。由于库是一个隔离的源代码片段,它只能访问调用合约的状态变量,如果它们被显式提供的话(否则就没有办法命名它们)。
库不能有状态变量;它们不支持继承,也不能接收以太。库可以包含结构和枚举。
一旦 Solidity 库部署到区块链上,任何人都可以使用它,假设您知道它的地址并且拥有源代码(仅具有原型或完整实现)。 Solidity 编译器需要源代码,以便它可以确保您正在尝试访问的方法确实存在于库中。
让我们来看一个例子:
library math
{
function addInt(int a, int b) returns (int c)
{
return a + b;
}
}
contract sample
{
function data() returns (int d)
{
return math.addInt(1, 2);
}
}
我们不能在合同源代码中添加库的地址;相反,在编译时需要将库地址提供给编译器。
库有许多用例。库的两个主要用例如下:
-
如果你有许多具有一些共同代码的合同,那么你可以将该共同代码部署为一个库。这样做可以节省 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): 销毁当前合同,将其资金发送到给定地址。
以太单位
字面上的数字可以以 wei、finney、szabo 或 以太 为后缀,以在以太的子单位间转换,没有后缀的以太货币数被假定是 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 编程语言。我们了解了数据位置、数据类型和合约的高级特性。我们还学习了编译和部署智能合约的最快最简单的方法。现在,您应该能够轻松地编写智能合约了。
在下一章中,我们将为智能合约构建一个前端,这将使部署智能合约和运行交易变得更容易。