背景图

以太坊智能合同搭建简易指南

本文是翻译的作品。适用于Truffle v3.4.11和Solidity v0.4.15。

我一直在使用4年的智能合约,主要是在比特币区块链中。我参与的一些项目是 Proof of ExistencebitcoreStreamium。在过去的几个月中,我一直在以太坊上探索和工作。

我已经决定编写一个简短的指南,以减轻未来程序员学习以太坊智能合约开发的难度。我已经将指南分为两部分:如何开始在以太坊建立智能合约以及关于智能合约安全的简要说明。

以太坊智能合约入门

基本概念

本指南假定您对加密货币和区块链的工作方式具有基本的技术理解。如果你还不了解,我建议你先阅读Andreas Antonopoulos的精通比特币书籍Consensys的“Just Etough Bitcoin for Ethereum”指南,或者至少观看Scott Driscoll的短片。要继续学习,您应该知道公钥和私钥是什么,为什么区块链需要矿工,如何达成分散化的共识,交易是什么,以及交易脚本和智能合约的概念。

在使用以太坊之前需要了解的另外两个相关重要的概念是以太坊虚拟机(EVM)和汽油(Gas)。

以太坊被设计成一个智能合约平台。它的起源实际上与Vitalik Buterin在比特币上作为非常有限的智能合约平台所作的批评有关。在以太坊中由以太坊虚拟机(EVM)来执行智能合约。与比特币相比,它为脚本提供了更富有表现力和完整的语言。事实上,它是一种图灵完备的编程语言。一个很好的比喻是,EVM是一个分布式的全球计算机,所有的智能合约都在这里被执行。

鉴于智能合约在EVM中运行,必须有一种机制来限制每个合同使用的资源。在EVM中执行的每一个操作实际上都是由网络中的每个节点同时执行的。这就是gas存在的原因。以太坊交易合同代码可触发数据读取和写入,执行昂贵的计算,如使用密码原语,向其他合同进行呼叫(发送消息)等。这些操作中的每一个都具有以gas量化的成本,并且交易消费的每个gas单位必须在Ether中支付,计算基于gas/ether价格动态变化。此价格从发送交易的以太坊帐户中扣除。交易还有一个gas_limit参数,这个参数是交易可以消耗多少天然气的上限,并被用作防止可能耗尽账户资金的编程错误的安全防范。你可以在这里阅读更多关于gas的知识

1.建立你的环境

那么你知道基础知识后,让我们使用代码来设置一切!要开始开发以太坊应用程序(或DApps,分布式应用程序,许多人喜欢这么称它们),您需要一个客户端来连接到网络。它将充当您分布式网络的窗口,并提供区块链的视图,视图将呈现所有EVM状态。

以太坊协议有各种兼容的客户端,最流行的是geth,一种Go语言的实现。但是,这不是最适合开发人员的。我发现的最好的选择是testrpc节点(名字确实很烂)。相信我,它会为你节省很多时间。安装并运行它(根据您的设置,您可能需要预先sudo安装):

1
2
$ npm install -g ethereumjs-testrpc 
$ testrpc

你应该运行testrpc一个新的终端,并在开发时保持运行。每次运行testrpc时,它都会生成10个带有模拟测试资金的新地址供您使用。这不是真正的金钱,你可以安全地尝试任何事情,没有丢失资金的风险。

在以太坊编写智能合同最流行的语言是solidity,因此我们将使用solidity。我们还使用了Truffle开发框架,该框架有助于创建智能合约,编译,部署和测试。让我们开始吧(再提一下,您可能需要sudo根据您的设置预先安装):

1
2
3
4
5
6
# 首先,我们安装truffle
$ npm install -g truffle
# 让我们来设置我们的项目
$ mkdir solidity-experiments
$ cd solidity-experiments/
$ truffle init

truffle将创建示例项目的所有文件,包括MetaCoin合约,一个代币合约示例。

通过运行truffle compile来编译示例合约。然后,要使用我们运行的testrpc节点将合约部署到模拟网络,需要运行truffle migrate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ truffle compile
Compiling ConvertLib.sol...
Compiling MetaCoin.sol...
Compiling Migrations.sol...
Writing artifacts to ./build/contracts
$ truffle migrate
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x686ed32f73afdf4a84298642c60e2002a6d0d736a5478cc8cb22a655ac018a67
Migrations: 0xa7edbac1156f98907a24d18df8104b5b1bd7027c
Saving successful migration to network...
... 0xe3bf1e50d2262d9ffb015091e5f2974c8ebe0d6fd0df97a7dbcde8a0e51c694a
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying ConvertLib...
... 0x2e0e6718f01d0da6da2ada13d6e4ad662c5a20e784e04c404e9d4ef1d392bdae
ConvertLib: 0xf4388ce4d4ce8a443228d65ecfa5149205db049f
Linking ConvertLib to MetaCoin
Deploying MetaCoin...
... 0xb03a3cde0672a2bd4dda6c01dd31641d95bd680c4e21162b3370ed6db7a5620d
MetaCoin: 0x4fc68713f7ac86bb84ac1ef1a09881a9b8d4100f
Saving successful migration to network...
... 0xb9a2245c27ff1c6506c0bc6349caf86a31bc9f700388defe04566b6d237b54b6
Saving artifacts...

Mac OS X用户注意:truffle有时会被.DS_Store文件混淆。如果您遇到提及其中某个文件的错误,请将其删除。

我们只是将示例合同部署到我们的testrpc节点。Wohoo!那很简单,对吧?现在是时候创建我们自己的合同了!

写第一个以太坊智能合约

在本指南中,我们将编写简单的存在证明的智能合约。这个想法是创建一个数字公证,存储文档哈希以证明其存在。使用truffle create contract开始吧:

1
$ truffle create contract ProofOfExistence1

现在用你最喜欢的文本编辑器打开contracts/ProofOfExistence1.sol(我使用带有Solidity语法高亮显示的vim),并粘贴这个代码的初始版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.15;
// 存在证明合同,版本1
contract ProofOfExistence1 {
// 状态
bytes32 public proof;
// 计算并存储文档证明
// *事务性函数*
function notarize(string document) {
proof = proofFor(document);
}
// 帮助函数获取文档的sha256
// *只读函数*
function proofFor(string document) constant returns (bytes32) {
return sha256(document);
}
}

我们将从简单但不正确的事情开始,并朝着更好的解决方案迈进。这是一个Solidity契约的定义,它就像其他编程语言中的一个类。合同具有状态和功能(函数)。区分合同中可能出现的两种功能非常重要:

  • 只读(constant)函数:不执行任何状态更改的函数。他们只读状态,执行计算并返回值。由于这些功能可以由每个节点本地解决,所以它们不需要gas。标有关键字constant。
  • 交易功能:执行合约状态变更或移动资金的功能。由于这些变化需要在区块链中得到体现,交易功能执行需要向网络发送交易并消耗天然气。

我们上面的合约中各种类型都有一个实例,文章注释中已经标记出来了。下一节,我们将学习如何通过方法的修饰符判断方法的类型,我们如何和智能合约交互。

这个简单的版本一次仅存储一个证明,使用数据类型bytes32(32个字节),这是sha256散列的大小。交易函数notarize允许将文档的散列存储在我们的智能合约的状态变量proof中。所述变量是公开的,并且也是我们合约用户验证文档是否已公证的唯一方式。我们很快就会做到这一点,但首先…

让我们部署ProofOfExistence1到网络中!这一次,您必须编辑迁移文件(migrations/2_deploy_contracts.js)以使Truffle部署我们的新合同。用以下内容替换文件中的内容:

1
2
3
4
var ProofOfExistence1 = artifacts.require("./ProofOfExistence1.sol");
module.exports = function(deployer) {
deployer.deploy(ProofOfExistence1);
};

为了再次运行此迁移,您需要使用重置flag标志,以确保它再次运行。

1
truffle migrate --reset

更多关于truffle迁移工作的信息可以在这里找到。

与您的智能合约交互

现在我们已经部署了合约,让我们操作这个合约吧!我们可以通过函数调用向它发送消息并查看它的公共状态。我们将使用solidity控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ truffle console
// 获得我们部署的合约
truffle(default)> var poe = ProofOfExistence1.at(ProofOfExistence1.address)
// 打印其地址
truffle(default)> poe.address
0x3d3bce79cccc331e9e095e8985def13651a86004
// 让我们注册我们的第一个“document”
truffle(default)> poe.notarize('An amazing idea')
{ tx: '0x18ac...cb1a',
receipt:
{ transactionHash: '0x18ac...cb1a',
...
},
logs: [] }
// 现在让我们得到该文件的证明
truffle(default)> poe.proofFor('An amazing idea')
0xa3287ff8d1abde95498962c4e1dd2f50a9f75bd8810bd591a64a387b93580ee7
// 检查合同的状态是否正确地被更改
truffle(default)> poe.proof()
0xa3287ff8d1abde95498962c4e1dd2f50a9f75bd8810bd591a64a387b93580ee7
// 散列符合我们之前计算的那个

我们所做的第一件事是获得我们部署的合同的表示并将其存储在一个名为poe的变量中。

然后我们调用交易方法notarize,这涉及到状态的改变。当我们调用一个交易函数时,我们得到一个Promise,它解析为一个交易对象,而不是实际函数返回的。请记住,要改变EVM状态,我们需要花费gas并向网络发送交易。这就是为什么我们得到一个交易信息对象作为Promise的结果,指的是做了这种状态变化的交易。在这种情况下,我们对交易ID没有兴趣,所以我们只丢弃Promise。在编写真实应用程序时,我们需要保存它以检查生成的事务并捕获错误。

接下来,我们调用只读(constant)函数proofFor。请记住用关键字标记您的只读功能constant,否则Truffle将尝试制定一个交易来执行它们。这是告诉我们Truffle没有与区块链互动但只是阅读区块链的一种方式。通过使用这个只读函数,我们获得了“An amazing idea”、“document”的sha256哈希。

我们现在需要与智能合约的状态进行对比。要检查状态是否被正确更改,我们需要读取proof公共状态变量。为了获得公共状态变量的值,我们可以调用一个同名的函数,该函数返回其值的Promise。在我们的例子中,输出哈希是相同的,所以一切都按预期工作:)。

有关如何与合约进行交互的更多信息,请阅读Truffle文档的这一部分

正如你可以从上面的代码片段看到的,我们的存在证明智能合约的第一个版本似乎正在工作!干得好!不过,它一次只能注册一个文档。我们来创建一个更好的版本。

迭代合同代码

让我们更改合同以支持多个文档证明。使用名称复制原始文件contracts/ProofOfExistence2.sol并应用这些更改。主要的变化是:我们将proof变量改为bytes32数组并将其称为证明,我们将其变为私有,并且我们添加一个函数通过迭代该数组来检查文档是否进行公证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity ^0.4.15;

// 证明存在契约,版本2
contract ProofOfExistence2 {
// 状态
bytes32[] private proofs;
// 在合约状态中存储存在证明
// *transactional function* 事务性函数
function storeProof(bytes32 proof) {
proofs.push(proof);
}
// 计算并存储文档证明
// *transactional function* 事务性函数
function notarize(string document) {
bytes32 proof = proofFor(document);
storeProof(proof);
}
// 帮助函数获取文档的sha256
// *read-only function* 只读函数
function proofFor(string document) constant returns (bytes32) {
return sha256(document);
}
// 检查一个文档是否已经过公证
// *read-only function* 只读函数
function checkDocument(string document) constant returns (bool) {
bytes32 proof = proofFor(document);
return hasProof(proof);
}
// 如果存储了证明,则返回true
// *read-only function* 只读函数
function hasProof(bytes32 proof) constant returns (bool) {
for (uint256 i = 0; i < proofs.length; i++) {
if (proofs[i] == proof) {
return true;
}
}
return false;
}
}

让我们与新功能进行交互:(请记住更新migrations/2_deploy_contracts.js以包含新合同并运行truffle migrate --reset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 部署合约
truffle(default)> migrate --reset
// 获取新版本的合同
truffle(default)> var poe = ProofOfExistence2.at(ProofOfExistence2.address)
// 让我们来检查一下新的文档,它不应该在那里。
truffle(default)> poe.checkDocument('hello')
false
// 让我们现在将该文档添加到证明存储
truffle(default)> poe.notarize('hello')
{ tx: '0x1d2d...413f',
receipt: { ... },
logs: []
}
// 现在让我们再次检查文档是否已公证!
truffle(default)> poe.checkDocument('hello')
true
// success!成功!
// 我们也可以存储其他文档,它们被记录为
truffle(default)> poe.notarize('some other document');
truffle(default)> poe.checkDocument('some other document')
true

这个版本比第一个更好,但仍然有一些问题。请注意,每次我们要检查文档是否经过公证时,我们都需要遍历所有现有的证明。这使得随着更多文件的添加,合同花费越来越多。存储证据的更好的结构是map。幸运的是,Solidity支持map,并称它们为映射。我们将在这个版本中改进的另一件事是删除标记为只读或事务函数的所有额外注释。我想你现在已经理解它了:)

这是最终版本,应该很容易理解,因为您遵循以前的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.4.15;
// 存在证明合同,版本3
contract ProofOfExistence3 {
mapping (bytes32 => bool) private proofs;
// store a proof of existence in the contract state
// 在合约状态中存储文档存在证明
function storeProof(bytes32 proof) {
proofs[proof] = true;
}
// 计算和存储文档证明
function notarize(string document) {
var proof = proofFor(document);
storeProof(proof);
}
// 帮助函数用于获取文档的sha256
function proofFor(string document) constant returns (bytes32) {
return sha256(document);
}
// 检查一个文档是否被公证过
function checkDocument(string document) constant returns (bool) {
var proof = proofFor(document);
return hasProof(proof);
}
// 如果存储了证明,则返回true
function hasProof(bytes32 proof) constant returns(bool) {
return proofs[proof];
}
}

这看起来不错。它和第二个版本完全一样。要试用它,请记住更新迁移文件并再次执行truffle migrate --reset。本教程中的所有代码都可以在此GitHub仓库中找到。

部署到真正的testnet网络

一旦您在模拟网络中使用testrpc广泛测试了您的合约,就可以在真实网络中尝试它!为此,我们需要一个真正的testnet/livenet以太坊客户端。按照这些说明安装geth

在开发过程中,你应该在testnet模式下运行节点,这样你就可以测试所有的东西而不用冒险花钱。Testnet模式(也被称为以太坊的Morden)与真实的以太坊基本相同,但以太币表示没有货币价值。不要偷懒,记得总是在testnet模式下开发,如果由于编程错误而失去真正的以太币,你会后悔的(相信我吧,嘿嘿)。

在testnet模式下运行geth,启用RPC服务器:

1
geth --testnet --rpc console 2 >> geth.log

这将打开一个控制台,您可以在其中键入基本命令来控制您的节点/客户端。您的节点将开始下载testnet区块链,并且您可以通过检查eth.blockNumber来检查进度。在区块链下载的同时,您仍然可以运行命令。例如,我们创建一个帐户:(记住密码!)

1
2
3
4
> personal.newAccount()
Passphrase:
Repeat passphrase:
"0xa88614166227d83c93f4c50be37150b9500d51fc"

让我们在那里发送一些货币并检查余额。你可以在这里免费获得testnet的以太币。只需复制粘贴您刚刚生成的地址,然后这里头会向您发送一些testnet以太币。要检查你的余额,运行:

1
2
> eth.getBalance(eth.accounts[0])
0

由于您的节点尚未与网络的其余部分同步,因此它不会显示余额。在您等待的时候,请在testnet区块浏览器中检查您的余额。在那里,你还可以看到当前testnet最高的块号(编写本文时为#1819865),你可以结合eth.blockNumber来知道你的节点何时完全同步。

一旦您的节点已同步,您就可以使用Truffle将合约部署到测试网络。首先,解锁您的主要帐户,以便truffle可以使用它。并确保它保持一定的余额,否则你将无法将新合约推送到网络。在geth运行:

1
2
3
4
> personal.unlockAccount(eth.accounts [0],“mypassword”,24 * 3600)
true
> eth.getBalance(eth.accounts [0])
1000000000000000000

准备好出发!如果上面的没有正常起作用,请检查上述步骤并确保您已正确完成。现在运行:

1
$ truffle migrate --reset

请注意,这次完成需要更长的时间,因为我们正在连接到实际的网络,而不是testrpc模拟的网络。一旦完成,您可以使用与之前相同的方法与合同进行交互。

可以在地址0xcaf216d1975f75ab3fed520e1e3325dac3e79e05处找到测试网部署的ProofOfExistence3版本。(随意与它交互并发送您的证明!)

我将把如何部署到实时网络的细节留给读者。你应该只有在模拟网络和测试网络中广泛测试合同。记住任何编程错误都可能导致livenet资金损失!

以太坊的智能合约安全很难

“做到智能合约正确真的很难。” EminGünSirer

天性使然,智能合约是定义资金流动的计算机代码,如果没有关于安全性的小记录,我不能结束本指南。我将在未来的帖子中更深入地讨论智能合约安全(编辑:像这样),但这里有一些快速注释来帮助你入门。

您应该注意的一些问题(并避免):

  • 重入:不要在合同中执行外部调用。如果你这样做,确保他们是你做的最后一件事。
  • 发送可能会失败:在发送货币时,您的代码应始终为发送功能失败做好准备。
  • 循环可能触发gas限制循环状态变量时要小心,这些状态变量可能会增大并且使气体消耗达到极限。
  • 调用堆栈深度限制:请勿使用递归,并且要注意,如果达到堆栈深度限制,任何调用都可能失败。编辑:这不再是一个问题
    时间戳依赖性不要在代码的关键部分使用时间戳,因为矿工可以操纵它们

这些仅仅是作为可能导致您的智能合约中的资金被盗用或破坏的意外行为的例子。真理是:如果你正在写智能合约,你正在编写处理真钱的代码。你应该非常小心!编写测试,做代码评论,并审核你的代码

避免显而易见的安全问题的最好方法是对语言有深入的了解。如果您有时间,我建议您阅读Solidity文档。我们仍然需要更好的工具来实现可接受的智能合约 (编辑:接近这篇文章的原始出版物,我们发布了OpenZeppelin库,并且我们最近发布了zeppelinOS

就写这些了!我希望您喜欢阅读本指南并了解您在以太坊编程智能合约的第一步!这仍然是一个非常新的行业,有很多新的应用程序和工具的空间。欢迎随时与我交流想法或原型。

如果您有兴趣讨论智能合约的安全,请加入我们的闲暇频道在Medium上关注我们,或申请与我们合作!我们也可以在智能合同安全开发审计工作中联系交流。

参考和引用

Manuel Araoz blog
The Hitchhiker’s Guide to Smart Contracts in Ethereum

0%