概述
面向全栈开发人员的端到端 dApp 教程
目录
面向全栈开发人员的端到端 dApp 教程
什么是 DApp?
后端(智能合约)
前端(Web 用户界面)
数据存储
IPFS
一群
去中心化消息通信协议
拍卖 DApp
拍卖 DApp:后端智能合约
DApp 治理
拍卖 DApp:前端用户界面
进一步分散拍卖 DApp
将拍卖 DApp 存储在 Swarm 上
准备蜂群
将文件上传到 Swarm
以太坊名称服务 (ENS)
ENS 规范
底层:名称所有者和解析者
中间层:.eth 节点
顶层:事迹
注册名称
管理您的 ENS 名称
ENS 解析器
将名称解析为 Swarm 哈希(内容)
从 App 到 DApp
结论
什么是 DApp?
DApp 是一种大部分或完全去中心化的应用程序。
考虑可能分散的应用程序的所有可能方面:
- 后端软件(应用程序逻辑)
- 前端软件
- 数据存储
- 消息通讯
- 名称解析
这些中的每一个都可以有些集中或有些分散。例如,前端可以开发为在集中式服务器上运行的 Web 应用程序,或作为在您的设备上运行的移动应用程序。后端和存储可以在私有服务器和专有数据库上,或者您可以使用智能合约和 P2P 存储。
创建 DApp 有很多典型的中心化架构无法提供的优势:
弹性:由于业务逻辑由智能合约控制,DApp 后端将在区块链平台上完全分布和管理。与部署在集中式服务器上的应用程序不同,DApp 不会停机,只要平台仍在运行,它就会继续可用。
透明度:DApp 的链上特性允许每个人检查代码并对其功能更加确定。与 DApp 的任何交互都将永久存储在区块链中。
审查阻力:只要用户有权访问以太坊节点(必要时运行一个),用户将始终能够与 DApp 进行交互,而不受任何集中控制的干扰。一旦代码部署在网络上,任何服务提供商,甚至智能合约的所有者都无法更改代码。
在今天的以太坊生态系统中,很少有真正去中心化的应用程序——大多数仍然依赖中心化服务和服务器来进行部分操作。未来,我们期望任何 DApp 的每个部分都可以以完全去中心化的方式运行。
后端(智能合约)
在 DApp 中,智能合约用于存储业务逻辑(程序代码)和应用程序的相关状态。您可以考虑用智能合约替换常规应用程序中的服务器端(也称为“后端”)组件。当然,这是过于简单化了。主要区别之一是在智能合约中执行的任何计算都非常昂贵,因此应尽可能少。因此,确定应用程序的哪些方面需要可信和分散的执行平台非常重要。
以太坊智能合约允许您构建架构,其中智能合约网络在彼此之间调用和传递数据,随时读取和写入自己的状态变量,其复杂性仅受区块气体限制的限制。部署智能合约后,您的业务逻辑很可能在未来被许多其他开发人员使用。
智能合约架构设计的一个主要考虑因素是智能合约一旦部署就无法更改代码。如果使用可访问的 SELF-DESTRUCT 操作码对其进行编程,则可以将其删除,但除了完全删除之外,不能以任何方式更改该代码。
智能合约架构设计的第二个主要考虑因素是 DApp 大小。一个非常大的单体智能合约可能会花费大量的气体来部署和使用。因此,一些应用程序可能会选择链下计算和外部数据源。但是请记住,让 DApp 的核心业务逻辑依赖于外部数据(例如,来自中央服务器)意味着您的用户必须信任这些外部资源。
前端(Web 用户界面)
与需要开发人员了解 EVM 和 Solidity 等新语言的 dApp 的业务逻辑不同,dApp 的客户端界面可以使用标准的 Web 技术(HTML、CSS、JavaScript 等)。这允许传统的 Web 开发人员使用熟悉的工具、库和框架。与以太坊的交互,例如签名消息、发送交易和管理密钥,通常是通过 Web 浏览器,通过 MetaMask 等扩展程序进行的。
虽然也可以创建移动 dApp,但目前帮助创建移动 dApp 前端的资源很少,主要是由于缺乏可以作为具有密钥管理功能的轻客户端的移动客户端。
前端通常通过 web3.js JavaScript 库链接到以太坊,该库与前端资源捆绑在一起,并由 Web 服务器提供给浏览器。
数据存储
由于高昂的 gas 成本和当前较低的区块 gas 限制,智能合约不太适合存储或处理大量数据。因此,大多数 DApp 使用链下数据存储服务,这意味着它们将大量数据从以太坊链上存储在数据存储平台上。该数据存储平台可以是中心化的(例如,典型的云数据库),也可以是去中心化的,存储在 P2P 平台(如 IPFS)或以太坊自己的 Swarm 平台上。
分散式 P2P 存储非常适合存储和分发大型静态资产,例如图像、视频以及应用程序前端 Web 界面的资源(HTML、CSS、JavaScript 等)。接下来,我们将看看其中的一些选项。
IPFS
星际文件系统 (IPFS) 是一种分散的内容可寻址存储系统,它在 P2P 网络中的对等点之间分配存储的对象。“内容可寻址”意味着每个内容(文件)都经过哈希处理,并且哈希用于标识该文件。然后,您可以通过其哈希请求从任何 IPFS 节点检索任何文件。
IPFS 旨在取代 HTTP 作为交付 Web 应用程序的首选协议。文件存储在 IPFS 上,并且可以从任何 IPFS 节点检索,而不是将 Web 应用程序存储在单个服务器上。
有关 IPFS 的更多信息,请访问https://ipfs.io。
一群
Swarm 是另一种内容可寻址的 P2P 存储系统,类似于 IPFS。Swarm 由以太坊基金会创建,作为 Go-Ethereum 工具套件的一部分。与 IPFS 一样,它允许您存储由 Swarm 节点传播和复制的文件。您可以通过哈希引用任何 Swarm 文件来访问它。Swarm 允许您从分散的 P2P 系统而不是中央 Web 服务器访问网站。
Swarm 的主页本身存储在 Swarm 上,可以在您的 Swarm 节点或网关上访问:https ://swarm-gateways.net/bzz:/theswarm.eth/ 。
去中心化消息通信协议
任何应用程序的另一个主要组件是进程间通信。这意味着能够在应用程序之间、应用程序的不同实例之间或应用程序的用户之间交换消息。传统上,这是通过依赖集中式服务器来实现的。然而,基于服务器的协议有多种去中心化的替代方案,通过 P2P 网络提供消息传递。DApps最著名的 P2P 消息传递协议是Whisper,它是以太坊基金会 Go-Ethereum 工具套件的一部分。
可以分散的应用程序的最后一个方面是名称解析。我们将在本章后面仔细研究以太坊的名称服务;不过,现在让我们深入研究一个示例。
拍卖 DApp
拍卖 DApp 允许用户注册“契约”代币,它代表一些独特的资产,例如房屋、汽车、商标等。一旦注册了代币,代币的所有权就会转移到拍卖会DApp,允许它上市出售。拍卖 DApp 列出了每个注册的代币,允许其他用户出价。在每次拍卖期间,用户可以加入专门为该拍卖创建的聊天室。拍卖完成后,契约代币所有权将转移给拍卖的获胜者。
整个拍卖过程如下图所示
我们拍卖 DApp 的主要组件是:
- 实现 ERC721 不可替代“契约”令牌的智能合约(DeedRepository)
- 实施拍卖 (
AuctionRepository
) 以出售契约的智能合约 - 使用 Vue/Vuetify JavaScript 框架的 Web 前端
- 用于连接以太坊链的 web3.js 库(通过 MetaMask 或其他客户端)
- 一个 Swarm 客户端,用于存储图像等资源
- Whisper 客户端,为所有参与者创建每次拍卖的聊天室
您可以在此处找到拍卖 dApp 的源代码。
拍卖 DApp:后端智能合约
我们的拍卖 DApp 示例由两个智能合约支持,我们需要在以太坊区块链上部署它们以支持应用程序:AuctionRepository 和 DeedRepository。
让我们从DeedRepository.sol: An ERC721 deed token for use in auction中显示的 DeedRepository 开始。该合约是与 ERC721 兼容的不可替代代币。
示例 1 DeedRepository.sol
.:用于拍卖的 ERC721 契约代币
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">链接:code/auction_dapp/backend/contracts/DeedRepository.sol[]</span></span></span>
如您所见,该DeedRepository
合约是 ERC721 兼容代币的简单实现。
我们的拍卖 DApp 使用DeedRepository
合约为每次拍卖发行和跟踪代币。拍卖本身是由 AuctionRepository 合约安排的。该合约太长,无法完整包含在此处,但是AuctionRepository.sol
: 主要的 Auction DApp 智能合约显示了合约的主要定义和数据结构。
示例 2 AuctionRepository.sol
.:主 Auction DApp 智能合约
该AuctionRepository
合约通过以下功能管理所有拍卖:
您可以使用本书存储库中的 Truffle 将这些合约部署到您选择的以太坊区块链(例如 Ropsten):
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">$ cd code/auction_dapp/backend
$ truffle init
$ truffle compile
$ truffle migrate --network ropsten</span></span></span>
DApp 治理
如果您通读 Auction dApp 的两个智能合约,您会注意到一些重要的事情:没有对 dApp 具有特殊权限的特殊帐户或角色。每个拍卖都有一个拥有一些特殊能力的所有者,但拍卖 dApp 本身没有特权用户。
这是一个深思熟虑的选择,目的是分散 dApp 的治理并在部署后放弃任何控制。相比之下,一些 dApp 拥有一个或多个具有特殊能力的特权帐户,例如能够终止 DApp 合约、覆盖或更改其配置或“否决”某些操作的能力。通常,在 dApp 中引入这些治理功能是为了避免由于错误而可能出现的未知问题。
治理问题是一个特别难以解决的问题,因为它是一把双刃剑。一方面,特权账户是危险的;如果受到破坏,他们可以破坏 DApp 的安全性。另一方面,没有任何特权帐户,如果发现错误,则没有恢复选项。我们已经在以太坊 DApp 中看到了这两种风险。在 The DAO 和以太坊分叉历史的案例中,有一些特权账户被称为“策展人”,但他们的能力非常有限。这些账户无法覆盖 DAO 攻击者提取的资金。在最近的一个案例中,去中心化交易所 Bancor 遭遇了大规模盗窃,因为一个特权管理账户被盗。事实证明,Bancor 并不像最初假设的那样去中心化。
在构建 DApp 时,您必须决定是否要让智能合约真正独立,启动它们然后无法控制,或者创建特权帐户并冒着被盗用的风险。任何一种选择都有风险,但从长远来看,真正的 DApp 不能对特权账户进行专门的访问——这不是去中心化的。
拍卖 DApp:前端用户界面
部署 Auction DApp 的合约后,您可以使用自己喜欢的 JavaScript 控制台和 web3.js 或其他 web3 库与它们进行交互。但是,大多数用户需要一个易于使用的界面。我们的拍卖 DApp 用户界面是使用 Google 的 Vue2/Vuetify JavaScript 框架构建的。
您可以在repo的 code/auction_dapp/frontend 文件夹中找到用户界面代码。该目录具有以下结构和内容:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">前端/
|-- 构建
| |-- 构建.js
| |-- 检查版本.js
| |-- 标志.png
| |-- utils.js
| |-- vue-loader.conf.js
| |-- webpack.base.conf.js
| |-- webpack.dev.conf.js
| `-- webpack.prod.conf.js
|-- 配置
| |-- dev.env.js
| |-- 索引.js
| `-- prod.env.js
|-- index.html
|-- package.json
|-- package-lock.json
|-- README.md
|-- src
| |-- 应用.vue
| |-- 组件
| | |-- 拍卖.vue
| | `-- 主页.vue
| |-- 配置.js
| |-- 合同
| | |-- AuctionRepository.json
| | `-- DeedRepository.json
| |-- main.js
| |-- 型号
| | |-- AuctionRepository.js
| | |-- 聊天室.js
| | `-- DeedRepository.js
| `-- 路由器
| `-- index.js</span></span></span>
部署合约后,编辑前端配置frontend/src/config.js
并输入已部署的DeedRepository
AuctionRepository 合约的地址。前端应用程序还需要访问提供 JSON-RPC 和 WebSockets 接口的以太坊节点。配置好前端后,使用本地计算机上的 Web 服务器启动它:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">$ npm 安装
$ npm 运行开发</span></span></span>
Auction dApp 前端将启动,并可通过任何网络浏览器在http://localhost:8080访问。
如果一切顺利,您应该会看到 Auction DApp 用户界面中显示的屏幕,该屏幕说明了在 Web 浏览器中运行的 Auction dApp。
前端
进一步分散拍卖 DApp
我们的 dApp 已经非常分散,但我们可以改进。
该AuctionRepository
合约独立运作,不受任何监督,对任何人开放。一旦部署它就无法停止,也无法控制任何拍卖。每个拍卖都有一个单独的聊天室,任何人都可以在没有审查或身份证明的情况下就拍卖进行交流。各种拍卖资产,例如描述和相关图像,存储在 Swarm 上,使其难以审查或阻止。
任何人都可以通过手动构建事务或在本地机器上运行 Vue 前端来与 dApp 交互。dApp 代码本身是开源的,并在公共存储库上协作开发。
我们可以做两件事来使这个 dApp 去中心化和有弹性:
- 将所有应用程序代码存储在 Swarm 或 IPFS 上。
- 使用以太坊名称服务通过引用名称来访问 dApp。
我们将在下一节探讨第一个选项,我们将在以太坊名称服务 (ENS) 中深入研究第二个选项。
将拍卖 DApp 存储在 Swarm 上
我们在本章前面介绍了 Swarm 中的 Swarm。我们的拍卖 dApp 已经使用 Swarm 来存储每次拍卖的图标图像。这是一个比试图在以太坊上存储数据更有效的解决方案,后者很昂贵。与将这些图像存储在网络服务器或文件服务器等集中式服务中相比,它也更具弹性。
但我们可以更进一步。我们可以将 dApp 本身的整个前端存储在 Swarm 中,并直接从 Swarm 节点运行它,而不是运行 Web 服务器。
准备蜂群
首先,您需要安装 Swarm 并初始化您的 Swarm 节点。Swarm 是以太坊基金会的 Go-Ethereum 工具套件的一部分。请参阅 [ ] 中有关安装 Go-Ethereum 的说明go_ethereum_geth
,或者要安装 Swarm 二进制版本,请按照Swarm 文档中的说明进行操作。
安装 Swarm 后,您可以使用 version 命令运行它来检查它是否正常工作:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">$ swarm version
版本:0.3
Git 提交:37685930d953bcbe023f9bc65b135a8d8b8f1488
Go 版本:go1.10.1
操作系统:linux</span></span></span>
要开始运行 Swarm,您必须告诉它如何连接到 Geth 实例,以访问 JSON-RPC API。按照入门指南中的说明开始。
当你启动 Swarm 时,你应该会看到如下内容:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">最大对等点数 ETH=25 LES=0 总计=25
启动对等节点实例=swarm/v0.3.1-225171a4/linux...
连接到 ENS API url=http://127.0.0.1:8545
swarm[ 5955]:[189B blob 数据]
启动 P2P 网络
UDP 侦听器 self=enode://f50c8e19ff841bcd5ce7d2d...
更新 bzz 本地地址 oaddr=9c40be8b83e648d50f40ad3...uaddr=e
启动 Swarm 服务
9c40be8b 蜂巢启动
检测到现有存储。尝试加载对等节点
配置单元 9c40be8b:对等节点已加载
Swarm 网络在 bzz 地址开始:9c40be8b83e648d50f40ad3d35f...
Pss 已启动
Streamer 已启动
IPC端点打开url=/home/ubuntu/.ethereum/bzzd.ipc
RLPx listener up self=enode://f50c8e19ff841bcd5ce7d2d...</span></span></span>
您可以通过连接到本地 Swarm 网关 Web 界面来确认您的 Swarm 节点是否正常运行:http://localhost:8500。
您应该会在 localhost 上看到类似于 Swarm 网关中的屏幕,并且能够查询任何 Swarm 哈希或 ENS 名称。
本地主机上的 Swarm 网关
将文件上传到 Swarm
一旦你的本地 Swarm 节点和网关运行,你可以上传到 Swarm 并且文件将可以在任何 Swarm 节点上访问,只需参考文件哈希。
让我们通过上传文件来测试一下:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">$ swarm up code/auction_dapp/README.md
ec13042c83ffc2fb5cb0aa8c53f770d36c9b3b35d0468a0c0a77c97016bb8d7c</span></span></span>
Swarm 已上传README.md
文件并返回一个哈希值,您可以使用该哈希值从任何 Swarm 节点访问该文件。例如,您可以使用公共 Swarm 网关。
虽然上传一个文件相对简单,但上传整个 dApp 前端就有点复杂了。这是因为各种 dApp 资源(HTML、CSS、JavaScript、库等)具有相互嵌入的引用。通常,Web 服务器将 URL 转换为本地文件并提供正确的资源。我们可以通过打包我们的 dApp 来为 Swarm 实现相同的目标。
在 Auction dApp 中,有一个用于打包所有资源的脚本:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">$ cd code/auction_dapp/frontend
$ npm run build</span><span style="color:#292929">> 前端@1.0.0 构建 /home/aantonop/Dev/ethereumbook/code/auction_dapp/frontend
> 节点构建/build.js</span><span style="color:#292929">Hash: 9ee134d8db3c44dd574d
Version: webpack 3.10.0
Time: 25665ms
Asset Size
static/js/vendor.77913f316aaf102cec11.js 1.25 MB
static/js/app.5396ead17892922422d4.js 502 kB
static/js/manifest.87447dd4f5e60a5f9652.js 1.54 kB
static/css /app.0e50d6a1d2b1ed4daa03d306ced779cc.css 1.13 kB
static/css/app.0e50d6a1d2b1ed4daa03d306ced779cc.css.map 2.54 kB
static/js/vendor.77913f316aaf102cec11.js.map 4.74 MB
static/js/app.5396ead17892922422d4.js.map 893 kB
static/js /manifest.87447dd4f5e60a5f9652.js.map 7.86 kB
index.html 1.15 kB</span><span style="color:#292929">构建完成。</span></span></span>
此命令的结果将是一个新目录,code/auction_dapp/frontend/dist
它包含整个 Auction DApp 前端,打包在一起:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">dist/
|-- index.html
`-- 静态
|-- css
| |-- app.0e50d6a1d2b1ed4daa03d306ced779cc.css
| `-- app.0e50d6a1d2b1ed4daa03d306ced779cc.css.map
`-- js
|-- app.5396ead17892922422d4.js
|-- app.5396ead17892922422d4.js.map
|-- manifest.87447dd4f5e60a5f9652.js
|-- manifest.87447dd4f5e60a5f9652.js.map
|-- 供应商.77913f316aaf102cec11.js
`-- 供应商.77913f316aaf102cec11.js.map</span></span></span>
现在,您可以使用 up 命令和 --recursive 选项将整个 dApp 上传到 Swarm。在这里,我们还告诉 Swarm index.html 是加载这个 dApp 的默认路径:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">$ swarm --bzzapi <a data-cke-saved-href="http://localhost:8500/" href="http://localhost:8500/" class="au li">http://localhost:8500</a> --recursive
--defaultpath dist/index.html up dist/</span><span style="color:#292929">ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581</span></span></span>
现在,我们的整个拍卖 dApp 都托管在 Swarm 上,可以通过 Swarm URL 访问:
- bzz://ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581
我们在分散我们的 DApp 方面取得了一些进展,但我们使它更难使用。像这样的 URL 比像auction_dapp.com
. 我们是否被迫牺牲可用性来获得去中心化?不必要。
在下一节中,我们将研究以太坊的名称服务,它允许我们使用易于阅读的名称,但仍然保留了我们应用程序的去中心化特性。
以太坊名称服务 (ENS)
你可以设计世界上最好的智能合约,但如果你不为用户提供一个好的界面,他们将无法访问它。
在传统互联网上,域名系统 (DNS) 允许我们在浏览器中使用人类可读的名称,同时在后台将这些名称解析为 IP 地址或其他标识符。在以太坊区块链上,以太坊命名系统(ENS)以去中心化的方式解决了同样的问题。
比如以太坊基金会的捐款地址0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359;
在一个支持ENS的钱包里,就是ethereum.eth
.
ENS 不仅仅是一个智能合约;它本身就是一个基本的 dApp,提供去中心化的名称服务。此外,许多 dApp 支持 ENS,用于注册、管理和拍卖注册名称。ENS 展示了 dApp 如何协同工作:它是为服务其他 dApp 而构建的 dApp,由 dApp 生态系统支持,嵌入到其他 dApp 中等等。
在本节中,我们将了解 ENS 的工作原理。我们将演示如何设置自己的名称并将其链接到钱包或以太坊地址,如何将 ENS 嵌入另一个 dApp,以及如何使用 ENS 命名您的 dApp 资源以使其更易于使用。
ENS 规范
ENS主要在三个以太坊改进提案中指定:EIP-137,它指定了ENS的基本功能;EIP-162,描述了 .eth 根的拍卖系统;和 EIP-181,它指定了地址的反向解析。
ENS 遵循“三明治”设计理念:底层是一个非常简单的层,后面是更复杂但可替换的代码层,顶层是一个非常简单的层,将所有资金保存在不同的账户中。
底层:名称所有者和解析者
ENS 在“节点”而不是人类可读的名称上运行:使用“Namehash”算法将人类可读的名称转换为节点。
ENS 的基础层是一个由 ERC137 定义的巧妙简单的合约(少于 50 行代码),它只允许节点的所有者设置有关其名称的信息并创建子节点(ENS 相当于 DNS 子域)。
基础层的唯一功能是使节点所有者能够设置有关其自己节点的信息(特别是解析器、生存时间或所有权转移)并创建新子节点的所有者。
Namehash 算法是一种递归算法,可以将任何名称转换为标识该名称的哈希。
“递归”是指我们通过求解一个同类型的较小问题的子问题来解决问题,然后用子问题的解来解决原问题。
Namehash 递归地散列名称的组成部分,为任何有效的输入域生成一个唯一的、固定长度的字符串(或“节点”)。例如 subdomain.example.eth 的 Namehash 节点是keccak('<example.eth>' node) + keccak('<subdomain>')
. 我们必须解决的子问题是计算节点 example.eth,即keccak('<.eth>' node) + keccak('<example>')
. 首先,我们必须计算 eth 的节点,即keccak(<root node>) + keccak('<eth>')
.
根节点就是我们所说的递归的“基本情况”,我们显然不能递归地定义它,否则算法永远不会终止!根节点定义为0x0000000000000000000000000000000000000000000000000000000000000000
(32 个零字节)。
把这一切放在一起,因此 subdomain.example.eth 的节点是keccak(keccak(keccak(0x0...0 + keccak('eth')) + keccak('example')) + keccak('subdomain'))
。
概括地说,我们可以定义 Namehash 函数如下(根节点的基本情况,或空名称,后跟递归步骤):
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">namehash([]) = 0x000000000000000000000000000000000000000000000000000
namehash([label, ...]) = keccak256(namehash(...) + keccak256(label))</span></span></span>
在 Python 中,这变成:
<span style="background-color:#f2f2f2"><span style="color:rgba(0, 0, 0, 0.8)"><span style="color:#292929">def namehash(name):
if name == '':
return '