首页 > 专题 > 区块链 > 正文

如何开始学习以太坊智能合约

2020-02-25 19:28:18
字体:
来源:转载
供稿:网友

       我已经智能合约领域工作了4年,主要在比特币区块链上。我参与的一些项目包括存在证明,bitcore(比特核心)以及Streamium. 过去这个月,我探索了在以太坊平台上进行开发。

       我决定制作一个简短的指南服务未来想要学习以太坊开发的程序员。手册分为两个部分:如何开始以太坊智能合约开发,智能合约安全简述.

       如何开始学习以太坊智能合约

       0.基础概念

这个指南假设你已经有了一些密码学货币和区块链的基础技术背景。 如果你没有,我建议快速过一遍Andreas Antonopoulos的《完全掌握比特币》(Mastering Bitcoin),Consensys的《用刚刚够的比特币来搞懂以太坊》(Just Enough Bitcoin for Ethereum),或者至少看看Scott Driscoll的短片。 为了继续读下去你得了解公钥和私钥,为什么区块链需要矿工,如何达成去中心化的共识,以及交易脚本和智能合约的概念。

另外两个在你开始进行以太坊开发之前需要了解的重要的,相关的概念是以太坊虚拟机和汽油(gas)。以太坊的目的在于成为一个智能合约平台。它的起源可以被追溯到Vitalik Buterin对比特币做为智能合约平台具有的局限性的评论。以太坊虚拟机(EVM)是以太坊智能合约执行之处。与比特币相比,它为撰写合约提供了更具表现力和完整性的语言。事实上,它是一个图灵完备的编程语言。一个比较好的比喻是,EVM是一个执行智能合约的分布式的世界电脑。由于智能合约由EVM执行, 必须存在一种限制每个合约占用资源的机制。EVM内运行的每一步操作实际上同时在被所有节点所执行。这是为什么需要有汽油(gas)存在。一个以太坊合约代码交易可以引发数据读写,密码学原语,调动(发送信息给)其他合约等等昂贵的运算。每个此类运算都有用汽油计量的价格,每笔交易所耗费的汽油单元需要用以太币来支付,根据随时变化的汽油和以太币的汇率计算。相应的价格会从提交交易请求的以太坊账户中扣除。同时每笔交易对可使用的汽油会设置上限参数,用以防止编程错误导致耗干账户中资金。点击这里阅读更多关于汽油。

       1.设置你的环境

好了,你已经知道了那些基础的,让我们赶紧把环境搞起来写代码吧。为了开始开发以太坊app(或者dapp,去中心化应用的简称,许多人喜欢这样叫),你需要安装一个客户端来接入主网。它会成为你进入这个分布式网络的窗口,提供一个观察区块链的方法,那里所有EVM(以太坊虚拟机)状态被显示出来。有很多与条款兼容的客户端,最受欢迎的是geth,用Go语言实现。但它并不是最开发者友好的客户端。我目前找到最好的选择是testrpc节点(是的,名字起得很糟糕)。相信我,它会节省你很多时间。安装它,运行它:

 

$ sudo npm install -g ethereumjs-testrpc$ testrpc

你应该在一个新的终端中运行‘testrpc’,并且在你开发的过程中一直让它运行。每次你运行testrpc,它会生成10个包涵模拟测试资金的新地址供你使用。这个不是真钱,你可以安全得用这些进行任何实验,不会有损失资金的风险。在以太坊中撰写智能合约最受欢迎的语言是Solidity,因此我们会使用这个语言。我们也会用Truffle开发框架,它会帮助创造智能合约,编译,部署以及测试。让我们开始吧

 

# First, let's install truffle首先,让我们安装truffle$ sudo npm install -g truffle# let's setup our project$ mkdir solidity-experiments$ cd solidity-experiments/$ truffle init

Truffle 会生成一个示范项目所需要的文件,包括MetaCoin,一个token合约的例子。你应该能够通过运行truffle compile指令来编译示范合约。然后,你需要通过我们在运行的testrpc节点用‘truffle migrate’指令来在模拟网络部署合约。

 

Compiling ConvertLib.sol...Compiling MetaCoin.sol...Compiling Migrations.sol...Writing artifacts to ./build/contracts$ truffle migrateRunning migration: 1_initial_migration.js  Deploying Migrations...  Migrations: 0x78102b69114dbb846200a6a55c2fce8b16f61a5dSaving successful migration to network...Saving artifacts...Running migration: 2_deploy_contracts.js  Deploying ConvertLib...  ConvertLib: 0xaa708272521f972b9ceced7e4b0dae92c77a49ad  Linking ConvertLib to MetaCoin  Deploying MetaCoin...  MetaCoin: 0xdd14d0691ca607d9a38f303501c5b0cf6c843fa1Saving successful migration to network...Saving artifacts...Note to Mac OS X users: Truffle is sometimes confused by .DS_Store files. If you get an error mentioning one of those files, just delete it.

我们刚刚往测试节点上部署了我们的示范合约。哇!很简单,对吧?是时候写我们自己的合约了!

       2.撰写你的第一个以太坊只能合约

在这个指南里面,我们会写一个存在证明只能合约。就是创造一个存有用于证明存在的文件哈希的电子公正机关。用‘truffle create:contract’来开始:

 

$ truffle create:contract ProofOfExistence1

从你的编译器里面打开合约/ProofOfExistnece1.sol(我用的是带Soilidity语法高亮显示的vim)

 

// Proof of Existence contract, version 1contract ProofOfExistence1 {  // state  bytes32 public proof;  // calculate and store the proof for a document  // *transactional function*  function notarize(string document) {    proof = calculateProof(document);  }// helper function to get a document's sha256  // *read-only function*  function calculateProof(string document) constant returns (bytes32) {    return sha256(document);  }}

我们将从一段简单但是有错误的代码开始向一个更好的解决方案靠近。这是一份Solidity合约定义,有点像其他语言中的类别(class)。合约中有状态(state)和函数(functions)。区分合约中可能出现的两种函数非常重要。

  • 只读(常数)函数:这些函数不对任何状态(state)进行改变。他们只读取状态,进行计算,并且返回数值。因为这些函数可以在每一个节点内本地解决,他们不回花费任何的汽油(gas)。他们被用‘contant’关键词标出。

  • 交易函数:这些函数对状态进行改变,转移资金。因为这些变化需要在区块链中被反应出来,执行交易函数需要向网络提交交易,这会消耗汽油(gas)。

我们的合约中两种函数各有一个,已在注释中标注。下一段我们将会看到我们使用函数的类型会如何改变我们与智能合约交互。这个简单的版本每次只储存一个证明,用数据类型bytes32或者32bytes,跟sha256哈希的大小一样。交易函数‘notarize’允许我们在合约的状态变量‘proof’里存储一个文件的哈希。这个变量是个公开变量,是我们合约的用户认证一个文件是否被公正的唯一途径。我们一会就会自己做一下,但是首先。。。
让我们把ProofOfExistence1部署到网络上!这次,你需要通过编辑移动文档(migration file)(migrations/2_deploy_contracts.js)让Truffle部署我们的新合约。用以下的来代替内容:

 

/* * migrations/2_deploy_contracts.js: */module.exports = function(deployer) {  deployer.deploy(ConvertLib);  deployer.autolink();  deployer.deploy(MetaCoin);  // add this line  deployer.deploy(ProofOfExistence1);};

你也可以选择性的删除有关ConvertLib和MetaCoin的语句,这些我们不会再用了。为了再次运行这个移动,你需要使用重启标签确保它再次运行。

 

truffle migrate --reset

更多的关于Truffle移动如何工作的内容可以看这里。

       3. 与你的智能合约互动

现在我们已经将智能合约部署好了,让我们摆弄摆弄它!我们可以通过函数调用来给它发信息或者读取它的公开状态。我们通过Truffle操纵台来完成:

 

$ truffle console// get the deployed version of our contracttruffle(default)> var poe = ProofOfExistence1.deployed()// and print its address truffle(default)> console.log(poe.address)0x3d3bce79cccc331e9e095e8985def13651a86004// let's register our first "document"truffle(default)> poe.notarize('An amazing idea')Promise { <pending> }// let's now get the proof for that documenttruffle(default)> poe.calculateProof('An amazing idea').then(console.log)Promise { <pending> }0xa3287ff8d1abde95498962c4e1dd2f50a9f75bd8810bd591a64a387b93580ee7// To check if the contract's state was correctly changed:truffle(default)> poe.proof().then(console.log)0xa3287ff8d1abde95498962c4e1dd2f50a9f75bd8810bd591a64a387b93580ee7// The hash matches the one we previously calculated

注意所有函数调用都会返回一个Promise,当Promise被解决如果我们想要检验它我们可以通过‘.then(console.log)’来输出。
我们要做的第一件事是获得一个我们部署合约的表达,并把它存储在一个叫做‘poe’的变量之中。
然后我们调用交易方程‘notarize’,这会涉及一个状态改变。当我们调用一个交易方程,我们得到的是一个被转化为交易id的Promise,而不是函数返回的值。记住为了改变EVM状态我们需要消耗汽油(gas)并且向网络提交一个交易。这是为什么我们会得到交易id做为Promise的结果,从改变状态的那项交易那里得到。在这里,我们对交易id不感兴趣,所以我们可以把Promise丢掉。不过当我们真正写app时,我们会想要把它存起来用以检查相应的交易,捕捉错误。

接下来,我们调用只读(常数)函数‘calculateProof‘. 记得用’constant‘关键词来标记你的只读函数,否则Truffle会试着创造一个交易来执行这个函数。这个是我们告诉Truffle,我们并没有跟区块链交互而只是在读取。通过这个只读函数,我们会得到’An amazing idea‘文件的sha256。

我们现在需要把这个和我们智能合约的状态进行对比。为了检查状态的改变是否正确,我们需要读取‘Proof’这个公开状态变量。要获得一个公开状态变量的值,我们得调用具有同样名字的一个函数,它会返回一个Promise。我们这次,输出的哈希值是一致的,所以一切都如我们所料得进行了 :)

像你从上面的片段看到的,我们第一版存在证明智能合约似乎可以工作!干得好!但是它每次只可以注册一个文件。让我们做一版更好的。

       4. 合约代码迭代

让我们修改合约来支持多个文件验证。把原文件复制到名为contracts/ProofOfExistence2.sol的新文件中,并且采取以下改变。主要的变化包括:我们把‘proof’变量变成了bytes32的数组,并且命名为‘proofs’,我们把它变成私有,然后加入一个通过循环访问数组来检查一个文件是否被公正的函数。

 

// Proof of Existence contract, version 2contract ProofOfExistence2 {  // state  bytes32[] private proofs;  // store a proof of existence in the contract state  // *transactional function*  function storeProof(bytes32 proof) {    proofs.push(proof);  }  // calculate and store the proof for a document  // *transactional function*  function notarize(string document) {    var proof = calculateProof(document);    storeProof(proof);  }  // helper function to get a document's sha256  // *read-only function*  function calculateProof(string document) constant returns (bytes32) {    return sha256(document);  }  // check if a document has been notarized  // *read-only function*  function checkDocument(string document) constant returns (bool) {    var proof = calculateProof(document);    return hasProof(proof);  }  // returns true if proof is stored  // *read-only function*  function hasProof(bytes32 proof) constant returns (bool) {    for (var i = 0; i < proofs.length; i++) {      if (proofs[i] == proof) {        return true;      }    }    return false;  }}

让我们与新的函数互动一下:(不要忘了更新migrations/2_deploy_contracts.js来加入新的合约并且运行‘truffle mirgrate--reset’)

 

// deploy contractstruffle(default)>  migrate --reset// Get the new version of the contracttruffle(default)> var poe = ProofOfExistence2.deployed()// let's check for some new document, and it shouldn't be there.truffle(default)> poe.checkDocument('hello').then(console.log)Promise { <pending> }false// let's now add that document to the proof storetruffle(default)> poe.notarize('hello')Promise { <pending> }// let's now check again if the document has been notarized!truffle(default)> poe.checkDocument('hello').then(console.log)Promise { <pending> }true// success!// we can also store other documents and they are recorded tootruffle(default)> poe.notarize('some other document');truffle(default)> poe.checkDocument('some other document').then(console.log)Promise { <pending> }true

这一版比第一版强,但是仍然有些问题。注意每一次我们想要检查一个文件是否有被公正过时都需要循环访问所有存在的‘proofs’。储存proofs更好的结构会是用映射(map)。走运的是,Solidity支持映射结构,在这个语言里称此结构为mappings。另外一个我们会在这一版代码做出的改进是我们会去掉那些多余的标识只读(read-only)或交易(transactional)函数的那些注释。我想现在你已经都知道这些了:)下面是最终版本,我想应该不难理解,因为是从之前的版本一点点变过来的:

 

// Proof of Existence contract, version 3contract ProofOfExistence3 {  mapping (bytes32 => bool) private proofs;  // store a proof of existence in the contract state  function storeProof(bytes32 proof) {    proofs[proof] = true;  }  // calculate and store the proof for a document  function notarize(string document) {    var proof = calculateProof(document);    storeProof(proof);  }  // helper function to get a document's sha256  function calculateProof(string document) constant returns (bytes32) {    return sha256(document);  }  // check if a document has been notarized  function checkDocument(string document) constant returns (bool) {    var proof = calculateProof(document);    return hasProof(proof);  }  // returns true if proof is stored  function hasProof(bytes32 proof) constant returns(bool) {    return proofs[proof];  }}

       这下看起来已经足够好了。它跟第二版运行起来没有差别。记得更新移动文档(migration file)同时再次运行‘truffle migrate -- reset’来测试一下它。这个教程中的所有代码都可以在这里找到。

       5.在真正的测试网络上部署

在你用testrpc在模拟网络上大量测试你的合约之后,你就可以在真正的网络上测试你的合约啦!这就需要你有一个真正的testnet/livenet以太坊客户端。点击这里看如何安装geth的说明。
开发的过程中,你应该在testnet模式中运行你的节点,这样你就可以在没有损失真金白银的风险下进行所有的测试。Testnet模式(在以太坊也叫Morden)基本上与真正的以太坊一模一样,但是这里的以太币token没有任何金钱价值。不要发懒,记得永远要在testnet模式下开发,如果你因为编程错误而损失以太币,你会非常后悔的。
在testnet模式下运行geth, 打开RPC服务器:

 

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

这会打开一个你可以输入基本口令来控制你的节点/客户端的控制器。你的节点会开始下载testnet区块链,你可以在eth.blockNumber上查看下载进度。区块链下载的同时,你仍然可以运行口令。比如,让我们设置一个账户:(千万要记住密码!)

 

> personal.newAccount()Passphrase:Repeat passphrase:"0xa88614166227d83c93f4c50be37150b9500d51fc"

让我们发送一些以太币过去并且查询余额。你可以从这里获得免费testnet以太币:https://zerogox.com/ethereum/wei_faucet. 只需复制粘帖你刚刚生成的那个地址,这个水龙头就是给你发送一个以太币。想要查询余额,运行以下代码:

 

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

它会告诉你没有余额因为你还没有与全网络同步。在你等待的同时,去testnet block explorer去查询一下余额。那里,你也可以看到testnet目前最高的块数(写这个的时候是#1355293),你可以将这个信息与eth.blockNumber的信息结合去判断你的节点是否已经完成同步。
一旦你的节点同步好,你就可以开始通过Truffle在testnet上部署你的合约了。首先,解锁你的主geth账户,这样Truffle就可以使用它。确认里面有一些余额,否则你将不能够把新的合约推向网络。

 

> personal.unlockAccount(eth.accounts[0], "mypassword", 24*3600)true> eth.getBalance(eth.accounts[0])1000000000000000000

准备好了吧!如果这两个的某一个无法运行,检查之前的步骤以确保你正确的完成了它们。现在,运行:

 

$ truffle migrate --reset

注意这次会需要更长的时间来完成,因为我们是在连接到真正的网络而不是一个用testrpc模拟出来的网络。一旦完成,你就可以用之前同样的方法跟智能合约互动。
在testnet上部署的版本ProofOfExistence3可以在这个地址找到:0xcaf216d1975f75ab3fed520e1e3325dac3e79e05.
我想把如何在以太坊现场网络部署合约的细节留给读者。你只应该在模拟网络和testnet大量测试你的合约之后再做这个。千万记得,任何编程错误都可能导致在livenet上的金钱损失!
以太坊中智能合约的安全性问题很具有挑战性。参见 Emin Gun Sirer的 “智能合约挺难弄对的”。
考虑到智能合约是定义金钱如何移动的电脑代码的性质,我不得不在安全问题上稍做提示。我会在以后的文章里深度的讨论合约安全性问题(像这里),但是这里我会先简单的提几点。

一些你应该知道(并且避免)的问题:

重入攻击(reentrancy):不要在合约里使用外部调用。如果迫不得已,确保它是你做得最后一件事。
发送失败(send can fail):发送资金时,你的代码应该为发送失败的情况做好准备。
循环可能引发汽油限制(Loops can trigger gas limit):当你在状态变量上做循环的时候千万当心,变量的大小会增长这可能导致汽油消耗到达极限。
调用栈深度限制(Call stack depth limit):不要使用递归,记住任何调用都可能因为调用栈到达极限而失败。
时间戳依赖性(Timestamp dependency):不用在代码的关键部分使用时间戳,因为矿工可以操纵它们。

这些是智能合约中可能导致资金盗窃以及毁坏的一些意外行为的例子。中心思想是:如果你在撰写智能合约,你就在写真正处理金钱的代码。你应该加一万个当心!写测试,反复检查代码,并且做代码审核。

避免明显安全问题的最好方法就是对语言有扎扎实实的理解。我建议熟读Solidity文档,如果你有时间。我们将会需要更多更好的工具来完善智能合约安全。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表