此文已由做者苏州受权网易云社区发布。html
欢迎访问网易云社区,了解更多网易技术产品运营经验git
1.智能合约的概述github
近几年,区块链概念的大风吹遍了全球各地,有的人以为这是一个大风口,有的人以为他是个泡沫。众所周知,比特币是区块链1.0,而以太坊被称为了区块链2.0,而区块链1.0和2.0最主要的差异就在于以太坊拥有了智能合约。其实,智能合约在1994年就已出现,计算机科学家和密码学家NickSzabo首次提出智能合约概念。早于区块链概念的诞生。Szabo描述了什么是“以数字形式指定的一系列承诺,包括各方履行这些许诺”的协议。虽然有它好处,但智能合约的想法一直未取得进展—主要是缺少可让它发挥出做用的区块链。web
从技术角度理解,智能合约实际上是一个语法简单、指令集精简的图灵完备的语言,就像简化版的JavaScript。智能合约和其余的语言的区别主要在于,一方面,智能合约和代币体系完美结合,可以完成一系列价值转移,另外一方面,智能合约会在全部节点统一执行,根据肯定的输入、肯定的代码保证肯定的输出,也是全部节点状态一致性的保证。最后是智能合约都由有外部触发调用,不存在什么定时调用等。编程
废话很少说,接下来,本人就从技术角度,来讲说智能合约方面的设计。设计模式
2.智能合约的分层设计安全
2.1分层设计说明性能优化
智能合约的分层设计模型主要是借鉴gitHub上的一篇名为《 浅谈以太访智能合约的设计模式与升级方法》文章的中心思想,其做者也是基于其多年的Java实战经验提出的一些智能合约设计思路。该文章有许多借鉴之处,但也存在许多坑点没有仔细考虑。文章的分层设计思路主要以下:服务器
“业务逻辑与外部解耦、业务逻辑与数据解耦”是Java设计模式的一种策略,也是其文章的主要思想。其实现方式主要将合约拆分为代理合约、业务控制合约、业务数据合约、命名控制器合约。其中代理合约是用于业务逻辑与外部Dapp的解耦,业务控制合约、业务数据合约和命名控制器合约是用于业务逻辑与数据的解耦。做者在设计时,也拆分了几种不一样的场景,详见以下:网络
控制器合约与数据合约1—>1:
控制器合约与数据合约1—>N:
控制器合约与数据合约N—>1:
控制器合约与数据合约N—>N:
此类状况能够拆解为上面三种状况的组合。
2.2分层设计实现关键点
1)合约与合约之间的调用
合约调用合约的实现主要有两种方式,第一种方式是能够经过 call、delegatecall、 callcode方法实现对其余合约的方法的调用,可是其弊端是使用存在安全性问题,并且不能获知被调用合约的执行结果,不建议使用。第二种方式,是经过在合约中“外部引用”被调用的外部合约进行实现。
经过合约“外部引用”实现调用外部合约须要注意如下几点:
合约对象中须要定义被应用合约对象的方法,不然合约中没法识别被应用对象,编译器会报错;
被引用对象须要经过合约对象的设置外部合约方法将合约对象进行引入,注意须要引入外部合约对象后。
2)合约与合约之间的转帐
合约能够接收转帐,须要显示声明回调函数,并在回调函数上加payable进行修饰。合约与合约之间进行转帐时,须要在合约中显示用send或者transfer进行合约之间的转帐,合约与合约之间的转帐将之内部交易的形式执行。另外,在显示转帐的方法中也须要加payable修饰。
pragma solidity ^0.4.2; contract Test{ function TTest(address contractAddress,uint amount) payable { contractAddress.transfer(amount); } function() payable { } } |
2.3分层设计的局限与问题
1)被调用合约的方法的数据返回限制
被调用合约在返回string/bytes等不定长类型时会存在问题。这种限制须要在设计被调用合约时要注意,在实际项目中业务逻辑合约和数据合约都属于被调用合约,故而其设计公共方法时须要规避string/bytes等不定长的限制问题。如下是一个调用失败的反例:
pragma solidity ^0.4.2; contract Test{ function TTest(address contractAddress,uint amount) { A a=A(contractAddress); //编译会报错 string temp=a.getString(); } } |
2)被调用合约的方法的返回参数长度限制
被调用合约在返回定长的数据时,不能返回超过32位长度的数据,例如bytes33/uint33编译器将会提示错误。
3)被调用合约结构体数据返回限制
Solidity语言中,在编译器0.4.17版本以后,能够支持struct结构体的数据返回。在返回结构体的状况下,编码须要注意添加“pragma experimental ABIEncoderV2;”,须要注意的是结构体中也不能包含string/bytes等不定长数据类型,可是返回struct这种形式还处于试验阶段,稳定性安全性有待论证。(在0.4.17版本以前不能使用由于之前编译器没有把struct做为一个真正的类,只是形式上的组合在一块儿)
pragma solidity ^0.4.17; pragma experimental ABIEncoderV2; contract Test{ struct MyStruct { int key; uint deleted; } function TTest() returns() { return MyStruct({key:int(1),deleted:uint(1)}); } } |
4)被调用合约返回合约类型的限制
被调用合约可以返回合约类型的数据,编译器将合约当作地址返回,而地址是定长的。
5)被调用合约事件监听的问题
若是被调用合约须要触发事件,可能会存在事件监听的问题。若是经过web3j监听区块链的事件,被调用的合约事件信息可能会被编码,故而可能致使web3j没法监听到被调用合约内部触发的事件。问题缘由为在定义的接口合约中没有相关的事件声明。(详见附录实例代码)例如如下是本人测试返回的事件信息:
//被调用合约的事件监听返回数据 { "data": "0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000bbf289d846208c16edc8474705c748aff07732db000000000000000000000000000000000000000000000000000000000000000e5365727669636520446f2e2e2e2e000000000000000000000000000000000000", "topics": [ "4d3a2e6362f7a2697702c4af6f5a55dbb398da05784a12752d3cb5e12dcbf965" ] } |
6)智能合约的传入参数大小限制
智能合约在传入参数方面存在着EVM虚拟机栈的限制,默认状况下EVM虚拟机栈的大小为1024*512bit,故而参数不能超过这个大小,不然会出现虚拟机栈。若是涉及的业务数据不大的状况下,能够在链上保存,若涉及的业务数据比较大,建议经过链外进行业务数据的交互。当下针对大业务数据比较好的一种解决方案是,经过IPFS文件系统存储线外数据。
(备注:IPFS(InterPlanetary File System,星际文件系统)是一个旨在建立持久且分布式存储和共享文件的网络传输协议。它是一种内容可寻址的对等超媒体分发协议。)
7)智能合约方法中局部变量的数量限制
在智能合约编程中,Solidity编译器不容许方法的超过16个“局部变量”,不然编译器将会报错。其中方法“局部变量”的计算规则自定义局部变量算1个,每一个传入传出参数算1个,外部合约调用算2个,总计不能超过16个,不然会出现“Stack Too Deep”的编译错误。
8)合约依赖外部库或者外部合约时部署限制
当某合约依赖外部库函数或者外部合约时,其部署合约时须要先部署外部库函数或者外部合约,将部署后获得的外部库或者外部合约地址设置到该合约的abi文件中。解决方式有两种,一种经过在该函数中定义设置外部函数或者外部合约的地址的方法,手动设置;另外一种是经过Truffle框架,其提供了依赖部署的方式,详见《Truffle使用手册》。
9)合约执行的出现Invalid Code的问题
针对使用assert断言或者require的函数修饰器(例如权限控制、启停控制等)程序判断不经过,将会执行revert语句,而revert语句在当前版本被认定为Invalid Code。
另外,说明下assert语句和require的区别:用了assert的话,则程序的gas limit会消耗完毕;而require的话,则只是消耗掉当前执行的gas。
3.智能合约的数据迁移
数据迁移问题也是设计系统必需要考虑的问题,而区块链的特色就是数据拥有不可窜改的特色,也就造就了智能合约数据迁移的困难。
1)继承式数据迁移法
新版本的数据合约中保存一个指向旧版本数据合约的合约地址,新版本数据合约保存的是增量的数据内容。该方法要求合约可以分层设计,将数据部分的合约独立出来。
2)日志式数据回放法
注意咱们不能新建一条链,并发全部的交易进行重放。此处指的是,在合约中经过event事件、结构体记录数据的状态及变化,必要时,可以新建一个合约并从新初始化一样的数据。
4.智能合约补救策略设计
编写以太访智能合约不免可能存在一些漏洞,假如系统遭受攻击造成资金损失,能够经过以下处理方式:
1)合约暂停或者销毁
在合约编码的时候,必定要给每个合约加上中止或者销毁的方法,便于在第一时间发现合约出现错误或者漏洞时损失的“停滞”。在暂停和销毁方法选择方面,本人建议都使用暂停方式,由于方法被暂停后调用方可以获得明确的异常通知,可是合约被销毁后,合约就不存在了,这时调用方继续调用会得不到异常反馈,且发送的资金也将永远不能被追回。
2)经过硬分叉
强制硬分叉,或者强制进行块数据回滚适用于联盟链的角度;并从新发布新合约;
3)合约数据迁移,从新发布新合约
在实践项目中,建议设置方法的启停开关,在出现异常状况下,能够及时中止合约方法,避免合约问题扩散。经过合约数据的迁移方式,从新发布问题合约。若是是代币,就从新发行新的代币,适用于公链或者联盟链。
5.智能合约安全性问题规避
1)合约之间的转帐send方法使用问题
在合约之间进行转帐操做时,若是使用<address>.send(value)方法时,该方法须要进行返回结果的判断,若是返回结果为false须要人工抛出异常,而后阻止后续流程,不然转帐异常后返回false仍是会继续执行后续流程,这种方式也能避免call deep合约攻击。
pragma solidity ^0.4.2; contract Test{ function TTest(address contractAddress,uint amount) payable { if(!contractAddress.send(amount)){throw;} } } |
2)合约的权限控制问题
合约的分层设计中,须要对依赖的外部合约进行手动注入,故而须要注意在合约的关键方法上进行权限控制,规避其余人能改变合约的调用关系,从而系统被攻击。
3)call、delegatecall、callcode方法使用问题
不建议在合约中使用call、delegatecall、callcode方法,由于这些方法可以调用代码未知,从而致使风险未知。
4)调用外部合约的顺序问题
在实现合约调用合约的模式中,须要注意的是,优先完成内部交易逻辑,将外部调用放在后面进行操做,这样能够避免call deep攻击。例如:Solidity官网文档中提到的Withdrawal模式。
5)交易执行顺序问题
交易顺序依赖就是智能合约的执行随着当前交易处理的顺序不一样而产生差别。在智能合约设计时须要考虑,交易的顺序性以及如何串联交易流程,例如经过设置全局业务的惟一标识。
6)问题合约的防范策略
每一个智能合约都不是百分百的完美,可能会存在一些漏洞或者Bug,针对有问题的合约,咱们须要第一时间能进行对合约的控制。好比在合约中增长“销毁函数”,第一时间销毁有问题合约,不过这种方式比较粗暴。另外一种方式,在合约方法中加入“启停”控制,当发现问题时,第一时间将合约的方法中止,而后尽快升级新合约,避免问题的蔓延。
7)被调用合约方法访问的约束策略
由于每一个调用的合约通常是有明确的调用的对象的,好比代理合约调用业务合约,那么就应该业务合约智能被代理合约调用,不然其余人只要知道了业务合约的地址,其也可直接发起调用,对合约的安全性存在影响。
6.智能合约实战问题记录
除了以上关于一些限制性的问题和安全漏洞方面的问题,在项目实战过程当中还遇到了一些其余问题,此处再也不分类,一并记录:
1)在用web3j调用的合约中含有自定义外部library的函数应用时,函数的监听无效,或者函数调用失败?
问题缘由:是由于当前合约在部署时须要依赖library部署后的地址,而用web3j部署合约时并未依赖library的地址,从而致使当前合约中的library没法调用,从而引起在引入library的函数中事件及方法都调用失败。
解决方式:1.经过增长设置library地址的函数,手动设置;2.经过Truffle等框架的依赖部署功能部署函数。
2)根据Solidity编译后的abi文件可以反编译为Solidity源码?
关于反编译Solidity代码的问题,如今是没有Solidity反编译器的,须要付出极大的努力才能使其看起来与原始源代码类似,只能经过看字节码反编译操做码,看程序的执行逻辑。
3)EVM在执行智能合约时,事物的回滚和提交的触发条件?
EVM在执行是能合约时在如下状况会进行抛出异常,进行回滚;由于EVM首先在快照(默克尔树)中执行代码,若是出现异常回滚将当前的快照回滚至原先的状态,回滚也会包括已经执行的金额退回给原帐户,可是须要注意的是事物回滚仍是会扣取执行交易消耗的gas费用,事物回滚异常以下:
Gas不够,抛出OutOfGasException,细分为如下三种;
-notEnoughOpGas
-notEnoughSpendingGas
-gasOverflow
指令非法,抛出IllegalOperationException;
寻址错误,抛出BadJumpDestinationException;
栈过小,抛出StackTooSmallException;
栈太大,抛出StackTooLargeException。
EVM在正确执行完如下指令,才能进行事物提交:
执行完STOP执令;
执行完RETURN执令;
执行完SUICIDE指令。
4)关于合约自毁后合约地址上的资金问题?
合约在进行自毁操做后,须要提供一个资金转向的地址,合约上的资金会转入该地址当中。另外,若是有帐户向销毁后的合约地址发送资金,将致使该笔资金被“冻结”且没法被追回的状况。
5)调用智能合约一个不存在的方法的不报错?
当调用一个外部合约时,且调用的方法不存在,包括方法名和方法参数没有匹配上时,Solidity会默认执行回调函数,回调函数若是不显示声明的状况下为一个没有方法名和返回参数的函数。
6)Remix没法链接EthereumJ测试链的问题?
首先在EthereumJ实现RPC的前提下(默认github源码是没有实现的),若是发现EthereumJ不能链接Remix是由于Remix先会发OPTIONS的请求“探测”下测试链,“探测”经过后在发net_listening的Post请求,因此在实现RPC请求时须要也实现OPTIONS请求方法,另外须要同时在Remix界面中打开listen on network。
若是Remix没法建立帐户,请在Remix的Setting中勾选“Always use Ethereum VM at load”和“Enable Personal Mode”。
7)智能合约中是否存在随机函数,或者不一样的机器获取的now时间不一致致使程序结果不一致?
在Solidity语言中规避了随机函数的存在,其设计的思想也是经过保障在一样的输入条件、程序代码的状况下能获得同样的结果,这也是每一个以太访节点的数据一致性的保证。在获取时间这个点上,now函数不是获取的系统的默认时间,而是取至block块的时间戳,从而每一个节点在收到网络中传播的块时,其获取到的now时间都是同样的。
8)EthereumJ中指定监听合约地址无效,仍是能监听到其余合约地址触发的事件?
由于在建立事件监听的时候,("address":["0x41bd05db83ed0645fac0995b11e8b734d7711b5c"]),地址被封装为List对象,EthereumJ会匹配address参数对象类型,List由于不能被匹配因此address参数没法被设置,致使建立的监听能监听其余地址的事件。
9)关于取消nonce致使发布的合约地址不变的问题?
由于在实际项目中对Ethereumj的版本进行调整,取消了nonce的限制,然而该智能合约在发布的过程当中会根据发送者的地址和发送者拥有的nonce生成合约地址,因此在发送者地址一致的和nonce一致的状况下,发布的合约的地址都是同一个,新发布的合约会覆盖久的合约,致使程序发布错误。
免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 SQL On Streaming
【推荐】 JavaScript 如何工做:渲染引擎和性能优化技巧