以太坊开发攻略(第三部分:安全性、局限性以及顾虑)

译者:朱晓玉

  这一年是2023年,Dave是一名即将失去工作的管理员,因为一个软件即服务的去中心化应用程序(SaaS Dapp)正在做他的工作,并且每个人都处在他这样的境遇中。尽管如此,作为一名忠诚的工作者,他还是要把他的工作做好,直到最后一天。Dave收到一封邮件,让他给一个智能合约函数发送一些以太币和一个地址。

  所以我们这位同志(Dave)打开他的以太坊钱包,也就是一个word文档。他打开myWallets.docx文档,发现里面有4条记录:我的密钥、公司的密钥、我的密码以及公司的密码。

  他打开Mist客户端,找到他需要发送以太币的智能合约,并从公司账户向该智能合约的函数发送1000以太币,完成之后他回到之前收到的邮件中,将他必须发送给智能合约函数的地址复制下来。但是,一不小心,他没有复制完这个地址。在意识到自己的错误之后,他手动将这个地址补充完整,但还是写错了最后一个字符。现在,这是一个完全新的地址。

  Dave按下了发送按钮,复制粘贴了公司的密码,并确认交易和函数的执行。在函数执行时,该函数将一些以太币发送到预定义的账户地址中,剩下的以太币将通过智能合约的一个自毁调用(self-destructing)发送到Dave之前输入的地址中,因为写这个程序的开发者认为这是一个很好的想法——在函数执行完之后,将现在已经没有价值的智能合约从区块链中清除掉(透露一下:事实上还做不到)。

  很巧的是,这个错误拼写的地址是这个教程第一部分Wrestling contract合约地址——是某人根据我们教程第二部分的第二种方法在主网上错误创建的合约地址。

  同样,在同一时刻,有人发现这个摔跤合约(Wrestling contract)并注册为第二个摔跤手,第二个摔跤手会投入一些以太币来调用Wrestle()函数,但是由于第一个摔跤手从来不开始他自己的回合,从而使得第二个摔跤手的以太币永远被锁在这个摔跤合约中(Wrestling contract)了。

  所以,在这种不太可能的场景下(是不太可能吗?),以太币的丢失是由于人为失误和合约的不完整。

关于摔跤合约(Wrestling contract)

  尽管我们已经完成了这个游戏的基础部分,但是我们没有考虑这个合约的生命周期——何时被生成,何时被调用,何时不复存在。

  如果一个摔跤手在玩过几个回合后不再参与了怎么办?所以我们应该给予选手一种能力——当其中一个选手不再参与他们的回合时,他们可以在一段时间后取回自己的金钱。

  我们同样需要考虑一个合约何时不再被使用的情况(在我们的案例中就是摔跤比赛的结束)。我曾说过,为了使一个合约能够接受以太币,我们应该给函数添加一个关键字“可支付”(“payable”),这样这个合约就可以接受币了。但是,一个合约仍可以通过下面这两种方式来接受以太币(并且你没办法拒绝它)——其中一个是当一个合约自毁(是一个特殊的,用Solidity预定义的函数,这个函数会使该合约失效并将剩余的所有的以太币发送到一个指定的账户地址)并将剩余以太币发送到另一个合约地址的时候,另一个是当这个智能合约接受挖矿所得的以太币的时候。所以,在开发的时候,你应该预留一种可以从智能合约中提取以太币方法(如果合约曾拥有的比本该拥有的还要多)。

  在我们的案例中,因为不管怎样赢者都可以从合约中拿走所有的以太币,所以我们可以让他使用另一个提取函数,类似于下面这个:

function withdraw() public {
    require(gameFinished && theWinner == msg.sender);

    msg.sender.transfer(this.balance);
}

  这个函数可以让他从智能合约中取走任意以太币,他想提取多少次都可以。

  一般我们在发送金钱的时候,请留心可能会因为一些原因导致发送失败,并且你也应该让合约的使用者自己提取他们的金钱而不是直接发送给他们。(就像我们在摔跤合约中做的那样)。

  同样,你可能会认为这是一个很棒的想法——让智能合约执行自毁,从而可以清理区块链,但是一个已执行自毁程序的智能合约仍保留在区块链上,并且依旧可以通过上述两种方法接受以太币。

  你也应该考虑一下B计划,以防因为一些原因,你的智能合约在执行过程中不能按照预期执行。因为一旦部署智能合约后,你就不能修改它了,你或许想保留一系列可以触发的锁定状态,并且这些锁定状态要么可以使得智能合约的交易停止,要么可以将它所掌握的以太币发送给另一个可以让合约用户提取以太币的智能合约(和上文中的例子一样)。这样的触发机制通过将权利交给第三方,从而削弱了智能合约的去中心化。所以是否采用这样的做法将取决于你的智能合约的具体使用场景。

安全性

  保证Solidity的安全性要从遵循常见开发模式开始,并与平台的开发保持一致,通过测试使得你的智能合约没有漏洞(至少尝试做到),认识到开发平台自身的局限性,让你的代码尽可能的保持简洁易懂,并且牢记于心的是以太坊本身(区块链背后的软件,Solidity编译器等)有很多漏洞,并且以太坊每天都在不断更新。

测试

  测试是任何正式开发过程中的一个重要部分,如果你曾使用的旧方法是等待漏洞出现后再去修复它,那么你可能会有一段艰难的适应过程。

  Truffle——在我们的上一个教程中见到过,让我们用这种简单的方式来测试我们的智能合约。

  打开上一教程的项目,接着打开一些命令行接口,然后运行ganache-cli

ganache-cli -p 7545

  创建一个新的文件夹,命名为“test”,然后在这个文件夹里面创建一个新的文件,命名为:“TestExample.js”。

  把下面的内容粘贴到新文件中:

var Wrestling = artifacts.require("Wrestling");

contract('Wrestling', function(accounts) {

	// "it" is the block to run a single test
	it("should not be able to withdraw ether", function() {
		Wrestling.deployed().then(function (inst) { 
			// We retrieve the instance of the deployed Wrestling contract
			wrestlingInstance = inst;

			var account0 = accounts[0];

			// how much ether the account has before running the following transaction
			var beforeWithdraw = web3.eth.getBalance(account0);

			// We try to use the function withdraw from the Wrestling contract
			// It should revert because the wrestling isn't finished 
			wrestlingInstance.withdraw({from: account0}).then(function (val) {
				assert(false, "should revert");
			}).catch(function (err) {
				// We expect a "revert" exception from the VM, because the user 
				// should not be able to withdraw ether
				console.log('Error: ' + err);

				// how much ether the account has after running the transaction
				var afterWithdraw = web3.eth.getBalance(account0);
				var diff = beforeWithdraw - afterWithdraw;

				// The account paid for gas to execute the transaction
				console.log('Difference: ' + web3.fromWei(diff, "ether"));
			})			
		})		
	})
});

  这段代码所做的是检索摔跤合约(Wrestling contract),并把它部署到我们的测试网络中,然后试着调用提款函数。因为在游戏结束前,没有人能够调用提款函数,并且我们在智能合约的提款函数中调用了require指令,所以它会返回一个错误值。

  在开发控制台上运行测试test:

truffle test --network development

  你应该会得到一个和下面一样的输出结果:

  因为执行合约函数是一个异步的过程,所以为了代码更加简洁,你可能会使用async/await。但是为了简单易懂,我们在例子中已经做了这个工作。

  给你留了一个练习,这个练习模拟了一个摔跤比赛,并且保证只有赢家能在比赛结束后从合约中取走以太币。(和我们在上一个教程中使用truffle控制台所做的操作很相似)。

  你会在本教程中看到另一个测试案例。同时,别犹豫了,赶紧去查看Truffle的文档吧!

  亦或,这里有一些社区开发的保障安全性的工具,这些工具可以帮助你们检查自己的代码。工具列表在此。

商业逻辑

  在智能合约内部测试函数是很好的做法,但是你应该停下脚步,回过头来看一下合约内部函数之间的交互,确认一下在整体上是否在做本该做的事情(并且没有执行其他事务)。

  确保智能合约有良好的注释是迈向代码整洁和编写清晰的第一步(注意,我们在第一部分教程中看到的智能合约Wrestling.sol就不是这样的)。第二步是尽可能地确保合约简洁易懂,并且只有你应用中需要去中心化的部分才写在智能合约中。如果你的智能合约是一个去中心化应用程序Dapp(简言之,一个去中心化的应用程序是一个部分结构是去中心化的web应用(也就是:它的一部分是智能合约,或者它与智能合约发生交互))的其中一部分,那就要区分哪些功能是需要在区块链上运行的,而哪些功能是通过用户界面或web应用程序的后端来实现的。

  值得注意的是:一个去中心化的应用程序的定义比上面提到的更要广泛,并且包括所有应用点对点交互的程序。Mist,Bittorrent,Tor等等这些都可以称为是去中心化的应用程序。可以参考一下Vitalik Buterin(以太坊创始人)的文章

了解平台

  若真的想了解自己到底在做什么,你应该阅读一下这些文档,这些都是你躲不掉的,并且你还要搜索这些文档没有覆盖到的有关Solidity开发方面的一些其他文章来充实自己的知识库。

  例如,你应该知道定点数变量(也就是浮点或双精度浮点)是Solidity暂时还不能完全支持的,并且在案例2中,一个使用了“uint”类型的除法,类似7/3,将取小于这个数并且最接近这个数的整数值(四舍五入下舍入)。所以在一个仍要进行多次开发的平台上,你不应该认为一些事情是理所当然的。

  因为区块链是公开透明的,每个人都可以知道你所掌握的变量的信息,并且你只能试图混淆其中信息的内容,而不能完全隐藏信息。对于产生一个随机数或一个字符串是同样的道理,了解你是如何试着生成随机数的任何人都可能伪造出这个数。事实上,人们正在尝试找出一个最好的方法来生成随机数,如果你有兴趣的话也可以参与其中。就像我之前说的,所有东西都还是在开发阶段,并且很多事情都没有工业化的标准。

  依赖时间的合约同样是个热点,如果你的合约需要在特定的某些时候运行,你可能需要依赖一个外部应用(需要记住的是这有时会导致合约终止或暂停),因为一个智能合约无法触发自己。如果你的合约在某些方面需要依赖时间来判断的话,请记住一些恶意的矿工会通过破坏执行交易的时间来进行攻击。

  请记住——你的合约是公开透明的,每个人都可以访问并与之交互,并且如果你的合约调用了外部的其他合约并与之交互,那么你必须紧记其他恶意的合约有可能会破坏你自身合约的执行。

  虽然所有这些都变得有点像詹姆斯邦德做事的风格(做事十分谨慎),但是当有一大笔钱处在不安全的状态时,人们就会变的很有创造力,想方设法地去获利。如果你的智能合约运行场景是一个商店销售老人纸尿裤,那么你可能就不需要关心那么多了。

平台的局限性

  要知道以太坊平台不用于复杂的计算,并且你的交易还受到气(gas)的限制。你应该尽可能地简化逻辑,并且要注意无限循环、存储限制、值溢出、以及所有这些的小细节。因为一旦智能合约部署到区块链上,你就不能删除或修改它了,你应该在部署合约之前考虑好所有的这些问题。

  编译器以及以太坊区块链背后的软件仍处在开发过程中,并且经历着不断更新,所以你应该记住这些,并保持不断更新。

第三方

  有很多优秀的开发者希望用户和其他开发者能够很轻松地进行操作,使其能够不用本地下载以太坊主链就可以访问以太坊网络。例如MEW——可以让你传送以太币并且部署智能合约,再比如与truffle相结合的INFURA,是一个很好的工具——可以让你通过一个API访问以太坊区块链。

  虽然这些服务的良好用意以及服务背后开发者们的技能是毋庸置疑的,但是否将追求便利性凌驾于追求安全性,就看你自己的选择了。像这样的平台总会成为黑客们的攻击目标,因为平台处理了许许多多的交易,所以平台总是比你自己在电脑上运行的一个节点更容易成为攻击目标。最后,你的选择有很大一部分会取决于你的操作和转账数额。

一些方向,以及开始于何处

  在你看完本文之前,这里有一些很棒的材料,你可以从这些材料开始你的研究:

  请记住一点,当提到安全性的时候,没有一份文档是能完全解决的,所以你必须通过深思熟虑和良好的编程实践自己去调研探究。

  最重要的是,开始加入到项目开发中吧,有很多项目需要更多开发者的眼睛来发现代码中的错误,并且还有很多赏金等着你们呢!

  如果你发现了漏洞,或者发现了一种更好地方法来做一些事情,不要犹豫要不要分享你的知识。以太坊和区块链社区都秉持着开放的原则,所以不管发现了什么或开发了什么,整个社区都会从中受益的。

  这部分代码你可以从Github上找到。

devzl/ethereum-walkthrough-3

ethereum-walkthrough-3 - Repository for the third part of the tutorial series on Ethereum, "Ethereum development…

github.com

总结

  总之,你必须要有智能合约开发者的思维,并且在你发布智能合约之前做好一系列的调研和测试工作。

  正如Moody教授所言:

image

  如果你喜欢这一部分,你可以在推特上关注我 @dev_zl

  下一部分教程,我们将介绍代币。