过渡到Web3 - 02 智能合约

892 阅读14分钟

译者:海洋饼干

原文链接


从web2转向web3 值得开发者信赖的教程 第二期

🙌

欢迎回到这个系列博客,该博客旨在帮助web开发者以尽可能简单的方法基于Ethereum(以太坊)构建分布式应用和智能合约。

本系列的第一篇博客为-过渡到Web3 - 01 分布式开发介绍.第二篇(也就是本篇)会讨论如何编写Ethereum(以太坊)智能合约,并附上了一个示例项目。


什么是智能合约?

正如第一篇博客提到的:

Ethereum背景下的智能合约是在一个公共节点的全球网络上执行的脚本 - EVM(Ethereum 虚拟机) - 并且可以从区块链读/写事务。

合约是“双方或多方间的自愿约定”([Wikipedia][10])。

智能合约是""一种推动谈判,校验谈判,或强制谈判,抑或执行合约的计算机协议""([Wikipedia][11])。

作为90年代的密码员,[Nick Szabo第一个提出了智能合约的概念][12],这个概念在许多以加密货币为基础的项目中都有抢眼表现,其中最引人注目的就是Ethereum(以太坊)。

智能合约的最初形式是自动售货机。自动售货机的潜在损失是有限的(最终的损失数目应该少于打破机制的耗费数目),即售货机吞入硬币,继而通过一个简单的机制,根据所显示的价格完成找零和出货。

现在可以给出基于Ethereum背景的定义:智能合约可被用于程序化和自动化执行合约。智能合约的意义在于取消了集中式中介,因此在分布式软件中可以发挥作用。

搭建

直接下载示例项目:

git clone https://github.com/lukehedger/todoeth.git

cd todoeth

git checkout issue#2-smart-contracts

我们将会看到一个加密经济诱因的Todo List — 它叫Todoeth。之后的两篇博客,我们会开始构建一个Todo List app,它通过扣掉用户的钱来推动大家完成自己的Todo List!

线框圈出的就是Todoeth。图标由[Hea Poh Lin][13]提供。

安装项目依赖需执行以下命令,接下来我们正式开始:

npm install

让我们了解下智能合约的开发流程。

设计

每个健壮的程序都以清晰完整的结构设计开始,这对编写智能合约尤其关键。鉴于部署到Ethereum区块链的合约不可改变,所有可能的情况必须考虑到。

在client-server式的web应用中,如果出现了一个bug,我们可以简单地给生产服务器打个补丁,这样我们网站的所有未来浏览者都会访问修补后的版本。考虑到Ethereum中数据具有永久性,可验证性以及防篡改性,很明显合约不能用这种方式简单升级 — 否则,所有记录都会丢失!

设计一个可更新的合约系统关键要将存储合约与代理合约分离(接下来会详细介绍)。

我们的分布式Todo app需要三个合约:

  • 一个用来存储每个用户以及用户Todo的初级合约
  • 一个追踪Todo存款的二级合约
  • 一个促进web应用交互的代理合约

编写

Solidty:有几种语言可以用来编写Ethereum智能合约,选择Solidty是因为它应用最为广泛。我们会使用0.4.18版本,这是编写合约时发布的最新稳定版本。[Atom][14] 与 [Sublime][15]均能提供语法高亮功能。

让我们以经典的“Hello,World”作为开始,当然,这里的实现是只对函数调用者说hello: )

pragma solidity ^0.4.18;
// import './SomeContract.sol';
contract HelloYou {

  event Hello(address you);

  

  function sayHello() public {

    address _person = msg.sender;

    

    Hello(_person);

  }

}

合约顶端的pragma是一个[指令][16],用于告知Solidty编译器我们使用了哪个版本。

这个合约没有任何输入,如果有的话,我们会在pragma下面用相关的文件路径来定义输入。

接下来是合约声明。为了声明一个合约,要为合约名称使用contract关键字 — 例子中,合约名称是'Hello You'。

合约包括一个eventfunction。事件用event关键字和一个函数签名(事件名称以及被括号括起来的类型参数)简单地声明。上例中事件名是‘Hello’,该事件接受一个类型为'address'的参数‘you’(address是Solidty中用来描述一个Ethereum公共地址的特殊类型)。

类型:与JS(动态类型)不同的是,Solidty是静态类型语言,必须定义每个变量的类型。如果你一直使用TypeScript或Flow编写JS(或者是已经使用的其他静态类型语言),你可以很快上手。即使你没用过,也能很快适应它。

sayHello函数使用了Solidty中的一个[全局命名空间的变量][17]msg.sender。这个值以及其他有用的值可被任何合约方法访问。因此,我们把这个值存入_person变量(不需使用var关键字,只需一个类型,变量名称和=),并把变量传递给Hello事件,它就可以触发EVM上的事件。

这样我们就通过一个智能合约对某个人说了hello。这个函数可以被调用,并且在web应用中这个事件可以被监听,下一篇文章中还会讲到!正如早先的Ethereum谚语所说:""如果没人在听,EVM上的事件还会发出声音吗?""

模式

就像之前提到的,我们必须用一种允许代码更新并且数据不会因此损失的方式构建我们的合约。实现方法是将包含永久状态(任何时候数据都存储在合约中)的代码与代理分离,而流入这些合约的数据都源自外部,比如web应用。这个存储合约可以保持很轻量的状态,并为一个不需要更新的操作提供服务。我们会在后面详细介绍存储合约。

添加一个新的Todo产生的数据流

**Todo.sol**

ToDo合约是我们对应存储合约的‘代理’,它也是唯一一个与我们的web app关联的合约。它不会保存任何数据。这样所有操作都与这个合约关联,并且任何更新都不会造成数据损失。

来看以下三种方法:

  • addTodo(todold) 把todo ID和msg.sender作为用户ID传递给ToDoStorage合约,并且把msg.value(一些token值)作为存款账户传递给ToDoBank
  • addToDo(index)TodoStorage合约中通过用户Todo数组获得一个Todo
  • getTodoCount() 获得一个用户存储的Todo数目

从存储中检索一个Todo被分为两步的原因是Solidty会限制返回动态长度的值,比如数组。因此,我们第一步要得到Todo数组的长度,接下来利用索引逐一获取Todo。但不必担心,这个限制在下一个版本0.5.0中会被移除。

存储

基于Ethereum区块链智能合约可以被用来存储不可变更的,可验证的数据。这是一个很棒的工具,可以应用于许多有趣的领域例如数字交易和协议。

在Todo示例中,我们会存储一个对应每个任务的永久引用!我们也会存储一些对应每个Todo的Ether(Ethereum的原生token)。

元数据存储:考虑到永久性和计算能力,在Ethereum上存储数据耗费很大。当然,能用许多创新方法来解除这种限制。我们会用到的一种普遍的模式,即在一个分布式存储系统(譬如[Swarm][18] 或 [IPFS][19])中存储元数据,同时存储对应于Ethereum中元数据的引用。下一篇文章将给出具体实现。

**TodoStorage.sol**

The TodoStorage contract simply contains methods for getting and setting todos. Metadata about the todo will be stored on Swarm and, so, the only data we actually store in the contract will be the Swarm reference to this metadata (an address of where to find the data in the Swarm network). TodoStorage 合约简单包括了获取和设置Todo的方法。关于Todo的元数据会存储在Swarm,因此我们实际存储于合约中的数据会是对应元数据的Swarm引用(即在Swarm网络中数据对应的地址)。

这些引用,我们会作为todoId存储在一个数组中,并且会在TodoStore对象中用userId锁上(Todo所有者的Ethereum地址)。

其数据结构如下:

TodoStore: {

  userId: [

    todoId

  ]

}

**TodoBank.sol**

存储使用的另一个合约是TodoBank。我们在这里存储对应每个Todo的Ether存款 - 这些存款在这里会被一直加密直到正常取消。当然,这个合约值包含获得和设置存款的方法。

数据结构如下:

TodoVault: {

  todoId: deposit

}

这已经不在示例的作用域之内,但你可以了解到激励机制是如何融入这个系统中的。例如,如果一个待办事项在特定时间被完成,钱就会退回所有者的地址,如果错过了截止日期,钱会被发送到另外一个对应用户未完成待办事项的地址。

因此,你必须完成Todo,否则就会失去你的钱。下篇文章会继续这方面的内容。

安全

当软件涉及个人数据和资金时安全就变得至关重要。鉴于智能合约的固有经济生态,以及编写合约所用的语言刚问世不久,他们的安全性极为重要 — 在设计和编写一个合约时必须建立安全机制。

当测试Solidty时,有必要警惕一些可能面对的针对合约的攻击。当你需要研究安全时,[Ethereum智能合约安全最佳实践][20]教程是个很棒的选择。

检查

你可以使用 [solium][21]检查你的合约,它是一个和ESLint类似的小工具。

在示例项目中检查合约需要运行以下命令:

npm run lint

文档

Solidty合约能以[Ethereum默认指定的格式]22使用注释作为文档,和JSDoc相似。

/*

 * @notice TodoAdded event

 * @param {bytes32} todoId

 */

event TodoAdded(bytes32 todoId);

编译

合约需要被Solidty编译器编译为可以被EVM读取的字节码

可以直接通过一个名为[solc-js][25]的JavaScript工具来使用Solidty编译器,该工具已经被Ethereum Foundation开发者使用。

编译器获得合约名称对象以及合约的字符化内容, [返回编译后的字节码][26] 和 [一个接入合约代码的接口,叫做ABI]27

我发觉这个过程可以简化,办法是对重复任务进行抽象(解析合约输入,将输出写入硬盘),集成为一个库,利用配置文件控制输入、输出。这个工具叫[Sulk][28] 😂!

Sulk的配置文件很简洁,下面是配置文件的最简形式:

module.exports = {

  contracts: [

    'Todo',

  ],

  inputPath: './path/to/contracts',

}

此外还需在项目中编写一个[contracts.json][29]文件,该文件包含了字节码和ABI,当‘应部署’你的合约时它就会派上用场。

编译项目示例中的合约需要运行以下命令:

npm run compile

应用

要想公开你的合约以及合约方法,你需要将合约部署到Ethereum区块链。这和部署一项微服务到服务器类似,Ethereum同样拥有生产,开发,以及本地网络的概念。生产网络 - 当然,只有一个生产网络-通常被视为‘主网络’;而开发网络-每个开发网络拥有不同的性质和能力-它们被称为‘测试网络’。

要开始与你合约的互动,最简单的方式就是将它部署到一个本地Ethereum网络。[**ganache-cli**][30](之前与testrpc齐名)是一个基于Node.js的“为Ethereum开发而生的个人区块链”,它简化了部署过程。

在项目示例中你可以运行以下命令,它会运行一个本地二进制安装的ganache-cli,且不需任何参数(没有比这更棒的了不是吗)

npm run 

一旦ganache开始运行,你就可以使用包含在项目示例中的部署脚本将合约部署到本地Ethereum节点:

npm run deploy

[部署脚本][31]使用了[Web3.js][32]库来与Ethereum节点实现交互。通读代码和注释,这样才能了解部署一项合约所需的步骤。要了关注工具是如何简化部署过程的。

Wb3.js:Web3.js库已经被Ethereum基金会采用,成为了备受欢迎的库,当然还有[ethers.js][33] 以及 [ethjs][34]等其他的库,它们都值得在项目示例中尝试。

部署脚本同样会在JSON文件中存储部署合约的地址,以便之后使用([addresses.json][35])。每个已部署的合约都拥有一个Ethereum地址(就像一个API拥有一个HTTP端点),当实例化的合约通过测试和应用实现互动时这些地址是必须的。

Gas

你可能在部署脚本和示例代码中见过‘gas’这个词或者是对它的引用。Gas只是为Ethereum操作收取的交易费用;是EVM的资金来源。最终,gas需要避免虚拟机器的过度使用。

Gas: 是一种与运算步骤类似的衡量手段。每笔交易都需要一个gas限额以及支付每个单位gas的费用。假若交易催生的运算使用的gas总量少于或等于限额,交易通过。如果超过了限额,所有的更改都会恢复为原样。

[github.com/ethereum/wi…]

测试

用JavaScript编写智能合约的单元测试是可行的。但是,不易让人接受的是在习惯于编写传统的client-server应用单元测试的程序员看来这有点违背常理:你必须运行一个本地Ethereum节点计算JavaScript测试中的合约方法。

关于模拟或存储网络响应的工具(比如适用于Ethereum的 [Sinon.JS][37])非常棒,一定会优先得到开发 - Go 和 Python已经有了类似的工具。现在,我们必须打破单元测试的根本限制并触发网络活动。

项目示例测试可以这样运行:

npm test

下一篇文章会详细介绍利用智能合约实现应用间的交互,现在你可以读读这些测试,以便了解如何使用Web3.js计算合约方法。

调试

更新合约代码,编译,部署以及运行单元测试的过程太过臃肿,debug效率很低。你可以使用Remix来debug,它是一个编写,编译,部署以及执行方法的IDE。

有个CLI工具也叫remixd,它可以与Remix IDE共享你的本地合约。项目示例中有执行共享操作的命令 - 然后点击 Remix工具栏中的🔗按钮进行连接:

npm run remix

你也可以 [将合约从Gist载入Remix][38]。


下一篇

我们会介绍如何构建一个与待办Todo List智能合约交互的分布式web应用。


拓展阅读

  • 📖 [Official Solidity docs][39]
  • 🏊 [Deeper dive into Solidity][40]
  • 🏪 [Solidity CRUD][41]
  • ☝️ [Writing upgradeable contracts][42]

项目更新

对非集中式应用痴迷的人运行了一个非常棒的副项目,项目的绝大部分代码已经开源。你可以深入挖掘并实践repo中的问题—我可没说一定要贡献你的代码 : )

在Jakk我们正在构建 [元网络和协议][43],最近在[一个私有alpha中实验][44],我们使用了合作伙伴的数据来填充网络。看下我们的代码[链接][45],在[Slack channel][46]和我们聊聊你发下的任何东西!

在[Twitter][48] 和 [Facebook][49] 上搜索 [我们][47]

感谢 [JAAK][50] 以及 [FRΞÐ Tibbles][51].

  • [Ethereum][52]
  • [Web3][53]
  • [Development][54]
  • [Dapp][55]
  • [Blockchain][56]

快给我们点赞吧,赞数的高低可以让我们判断哪篇文章更受欢迎。

[3]: twitter.com/jaak_io ""Visit “JAAK” on Twitter"" [4]: blog.jaak.io//facebook.c… ""Visit “JAAK” on Facebook"" [5]: medium.com/m/signin?re… [6]: medium.com/m/signin?re… [7]: blog.jaak.io/@level_out?… [8]: blog.jaak.io/@level_out?… [9]: blog.jaak.io/crossing-ov… [10]: en.wikipedia.org/wiki/Contra… [11]: en.wikipedia.org/wiki/Smart_… [12]: www.fon.hum.uva.nl/rob/Courses… [13]: thenounproject.com/charlenehea… ""View Profile"" [14]: atom.io/packages/la… [15]: packagecontrol.io/packages/Et… [16]: en.wikipedia.org/wiki/Direct… [17]: solidity.readthedocs.io/en/develop/… [18]: swarm-gateways.net/bzz:/theswa… [19]: ipfs.io/ [20]: consensys.github.io/smart-contr… [21]: github.com/duaraghav8/… [22]: github.com/ethereum/wi… [23]: twitter.com/notice ""Twitter profile for @notice"" [24]: twitter.com/param ""Twitter profile for @param"" [25]: github.com/ethereum/so… [26]: solidity.readthedocs.io/en/develop/… [27]: solidity.readthedocs.io/en/develop/… [28]: github.com/lukehedger/… [29]: github.com/lukehedger/… [30]: github.com/trufflesuit… [31]: github.com/lukehedger/… [32]: github.com/ethereum/we… [33]: github.com/L4ventures/… [34]: github.com/ethjs/ethjs [35]: github.com/lukehedger/… [36]: github.com/ethereum/wi… [37]: sinonjs.org/ [38]: gist.github.com/lukehedger/… [39]: solidity.readthedocs.io/en/develop/… [40]: github.com/androlo/sol… [41]: medium.com/@robhitchen… [42]: blog.colony.io/writing-upg… [43]: github.com/meta-networ… [44]: cointelegraph.com/news/jaak-a… [45]: github.com/meta-networ… [46]: community.meta-network.io/ [47]: jaak.io [48]: twitter.com/jaak_io [49]: www.facebook.com/JAAK.io/ [50]: medium.com/@jaak_io?so… [51]: medium.com/@Tibbles?so… [52]: blog.jaak.io/tagged/ethe… [53]: blog.jaak.io/tagged/web3… [54]: blog.jaak.io/tagged/deve… [55]: blog.jaak.io/tagged/dapp… [56]: blog.jaak.io/tagged/bloc… [57]: blog.jaak.io/@level_out?… ""Go to the profile of Luke Hedger"" [58]: blog.jaak.io/@level_out ""Go to the profile of Luke Hedger"" [59]: blog.jaak.io?source=footer_card ""Go to JAAK"" [60]: blog.jaak.io?source=footer_card [61]: blog.jaak.io ""Go to JAAK"" [62]: medium.com/@Medium/per…