前言
作为一名搬砖多年的资深码农,刚开始接触Solidity便感觉无从下手:低廉的计算和存储资源、简陋的语法个性、令人抓狂的debug体验、近乎瘠薄的类库反对、一言不合就插入汇编语句……让人不禁狐疑,这都曾经过了9012年了,竟然还有这种反人类的语言?
对于习惯应用各类日益“傻瓜化”的类库和自动化高级框架的码农而言,学习Solidity的过程就是一场一言难尽的劝退之旅。
但随着对区块链底层技术的深刻学习,大家会缓缓了解作为运行在“The World Machine”上的Solidity语言,必须要严格遵循的设计准则以及衡量后必须付出的代价。
正如黑客帝国中那句驰名的slogan:“Welcome to the dessert of the real”,在顽劣艰辛的环境背后,最重要的是学会如何适应环境、保留本身并疾速进化。
本文总结了一些Solidity编程的攻略,期待各位读者不吝分享交换,达到抛砖引玉之效。
上链的准则
“如无必要,勿增实体”。
基于区块链技术及智能合约倒退现状,数据的上链需遵循以下准则:
- 须要分布式合作的重要数据才上链,非必须数据不上链;
- 敏感数据脱敏或加密后上链(视数据窃密水平抉择合乎隐衷爱护安全等级要求的加密算法);
- 链上验证,链下受权。
如果在智能合约中定义了简单的逻辑,特地是合约内定义了简单的函数入参、变量和返回值,就会在编译的时候碰到以下谬误:
Compiler error: Stack too deep, try removing local variables.
这也是社区中的高频技术问题之一。造成这个问题的起因就是EVM所设计用于最大的栈深度为16。
所有的计算都在一个栈内执行,对栈的拜访只限于其顶端,限度形式为:容许拷贝最顶端16个元素中的一个到栈顶,或者将栈顶元素和上面16个元素中的一个替换。
所有其余操作都只能取最顶的几个元素,运算后,把后果压入栈顶。当然能够把栈上的元素放到存储或内存中。但无奈只拜访栈上指定深度的那个元素,除非先从栈顶移除其余元素。如果一个合约中,入参、返回值、外部变量的大小超过了16个,显然就超出了栈的最大深度。
因而,咱们能够应用构造体或数组来封装入参或返回值,达到缩小栈顶元素应用的目标,从而防止此谬误。
例如以下代码,通过应用bytes数组来封装了本来16个bytes变量。
function doBiz(bytes[] paras) public { require(paras.length >= 16); // do something}
保障参数和行为合乎预期
心怀“Code is law”的远大理想,极客们设计和发明了区块链的智能合约。
在联盟链中,不同的参与者能够应用智能合约来定义和书写一部分业务或交互的逻辑,以实现局部社会或商业活动。
相比于传统软件开发,智能合约对函数参数和行为的安全性要求更为严格。在联盟链中提供了身份实名和CA证书等机制,能够无效定位和监管所有参与者。不过,智能合约不足对破绽和攻打的事先干涉机制。正所谓字字珠玑,如果不谨严地查看智能合约输出参数或行为,有可能会触发一些意想不到的bug。
因而,在编写智能合约时,肯定要留神对合约参数和行为的查看,尤其是那些对外部凋谢的合约函数。
Solidity提供了require、revert、assert等关键字来进行异样的检测和解决。一旦检测并发现错误,整个函数调用会被回滚,所有状态批改都会被回退,就像从未调用过函数一样。
以下别离应用了三个关键字,实现了雷同的语义。
require(_data == data, "require data is valid");if(_data != data) { revert("require data is valid"); }assert(_data == data);
不过,这三个关键字个别实用于不同的应用场景:
- require:最罕用的检测关键字,用来验证输出参数和调用函数后果是否非法。
- revert:实用在某个分支判断的场景下。
- assert: 查看后果是否正确、非法,个别用于函数结尾。
在一个合约的函数中,能够应用函数润饰器来形象局部参数和条件的查看。在函数体内,能够对运行状态应用if-else等判断语句进行查看,对异样的分支应用revert回退。在函数运行完结前,能够应用assert对执行后果或中间状态进行断言查看。
在实践中,举荐应用require关键字,并将条件查看移到函数润饰器中去;这样能够让函数的职责更为繁多,更专一到业务逻辑中。同时,函数润饰器等条件代码也更容易被复用,合约也会更加平安、层次化。
在本文中,咱们以一个水果店库存管理系统为例,设计一个水果超市的合约。这个合约只蕴含了对店内所有水果品类和库存数量的治理,setFruitStock函数提供了对应水果库存设置的函数。在这个合约中,咱们须要查看传入的参数,即水果名称不能为空。
pragma solidity ^0.4.25;contract FruitStore { mapping(bytes => uint) _fruitStock; modifier validFruitName(bytes fruitName) { require(fruitName.length > 0, "fruite name is invalid!"); _; } function setFruitStock(bytes fruitName, uint stock) validFruitName(fruitName) external { _fruitStock[fruitName] = stock; }}
如上所述,咱们增加了函数执行前的参数查看的函数润饰器。同理,通过应用函数执行前和函数执行后查看的函数润饰器,能够保障智能合约更加平安、清晰。智能合约的编写须要设置严格的前置和后置函数查看,来保障其安全性。
严控函数的执行权限
如果说智能合约的参数和行为检测提供了动态的合约安全措施,那么合约权限管制的模式则提供了动静拜访行为的管制。
因为智能合约是公布到区块链上,所有数据和函数对所有参与者都是公开通明的,任一节点参与者都可发动交易,无奈保障合约的隐衷。因而,合约发布者必须对函数设计严格的拜访限度机制。
Solidity提供了函数可见性修饰符、润饰器等语法,灵便地应用这些语法,可帮忙构建起非法受权、受控调用的智能合约零碎。
还是以方才的水果合约为例。当初getStock提供了查问具体水果库存数量的函数。
pragma solidity ^0.4.25;contract FruitStore { mapping(bytes => uint) _fruitStock; modifier validFruitName(bytes fruitName) { require(fruitName.length > 0, "fruite name is invalid!"); _; } function getStock(bytes fruit) external view returns(uint) { return _fruitStock[fruit]; } function setFruitStock(bytes fruitName, uint stock) validFruitName(fruitName) external { _fruitStock[fruitName] = stock; }}
水果店老板将这个合约公布到了链上。然而,公布之后,setFruitStock函数可被任何其余联盟链的参与者调用。
尽管联盟链的参与者是实名认证且可预先追责;但一旦有歹意攻击者对水果店发动攻打,调用setFruitStock函数就能任意批改水果库存,甚至将所有水果库存清零,这将对水果店失常经营治理产生严重后果。
因而,设置某些预防和受权的措施很必要:对于批改库存的函数setFruitStock,可在函数执行前对调用者进行鉴权。
相似的,这些查看可能会被多个批改数据的函数复用,应用一个onlyOwner的润饰器就能够形象此查看。_owner字段代表了合约的所有者,会在合约构造函数中被初始化。应用public润饰getter查问函数,就能够通过_owner()函数查问合约的所有者。
contract FruitStore { address public _owner; mapping(bytes => uint) _fruitStock; constructor() public { _owner = msg.sender; } modifier validFruitName(bytes fruitName) { require(fruitName.length > 0, "fruite name is invalid!"); _; } // 鉴权函数润饰器 modifier onlyOwner() { require(msg.sender == _owner, "Auth: only owner is authorized."); _; } function getStock(bytes fruit) external view returns(uint) { return _fruitStock[fruit]; } // 增加了onlyOwner润饰器 function setFruitStock(bytes fruitName, uint stock) onlyOwner validFruitName(fruitName) external { _fruitStock[fruitName] = stock; }}
这样一来,咱们能够将相应的函数调用权限查看封装到润饰器中,智能合约会主动发动对调用者身份验证查看,并且只容许合约部署者来调用setFruitStock函数,以此保障合约函数向指定调用者凋谢。
形象通用的业务逻辑
剖析上述FruitStore合约,咱们发现合约里仿佛混入了奇怪的货色。参考繁多职责的编程准则,水果店库存治理合约多了上述函数性能查看的逻辑,使合约无奈将所有代码专一在本身业务逻辑中。
对此,咱们能够形象出可复用的性能,利用Solidity的继承机制继承最终形象的合约。
基于上述FruitStore合约,可形象出一个BasicAuth合约,此合约蕴含之前onlyOwner的润饰器和相干性能接口。
contract BasicAuth { address public _owner; constructor() public { _owner = msg.sender; } function setOwner(address owner) public onlyOwner{ _owner = owner; } modifier onlyOwner() { require(msg.sender == _owner, "BasicAuth: only owner is authorized."); _; }}
FruitStore能够复用这个润饰器,并将合约代码收敛到本身业务逻辑中。
import "./BasicAuth.sol";contract FruitStore is BasicAuth { mapping(bytes => uint) _fruitStock; function setFruitStock(bytes fruitName, uint stock) onlyOwner validFruitName(fruitName) external { _fruitStock[fruitName] = stock; }}
这样一来,FruitStore的逻辑被大大简化,合约代码更精简、聚焦和清晰。
预防私钥的失落
在区块链中调用合约函数的形式有两种:外部调用和内部调用。
出于隐衷爱护和权限管制,业务合约会定义一个合约所有者。假如用户A部署了FruitStore合约,那上述合约owner就是部署者A的内部账户地址。这个地址由内部账户的私钥计算生成。
然而,在事实世界中,私钥泄露、失落的景象亘古未有。一个商用区块链DAPP须要庄重思考私钥的替换和重置等问题。
这个问题最为简略直观的解决办法是增加一个备用私钥。这个备用私钥可反对权限合约批改owner的操作,代码如下:
contract BasicAuth { address public _owner; address public _bakOwner; constructor(address bakOwner) public { _owner = msg.sender; _bakOwner = bakOwner; } function setOwner(address owner) public canSetOwner{ _owner = owner; } function setBakOwner(address owner) public canSetOwner{ _bakOwner = owner; } // ... modifier isAuthorized() { require(msg.sender == _owner || msg.sender == _bakOwner, "BasicAuth: only owner or back owner is authorized."); _; }}
这样,当发现私钥失落或泄露时,咱们能够应用备用内部账户调用setOwner重置账号,复原、保障业务失常运行。
面向接口编程
上述私钥备份理念值得推崇,不过其具体实现形式存在肯定局限性,在很多业务场景下,显得过于简略粗犷。
对于理论的商业场景,私钥的备份和保留须要思考的维度和因素要简单得多,对应密钥备份策略也更多元化。
以水果店为例,有的连锁水果店可能心愿通过品牌总部来治理私钥,也有的可能通过社交关系重置帐号,还有的可能会绑定一个社交平台的治理帐号……
面向接口编程,而不依赖具体的实现细节,能够无效躲避这个问题。例如,咱们利用接口性能首先定义一个判断权限的形象接口:
contract Authority { function canCall( address src, address dst, bytes4 sig ) public view returns (bool);}
这个canCall函数涵盖了函数调用者地址、指标调用合约的地址和函数签名,函数返回一个bool的后果。这蕴含了合约鉴权所有必要的参数。
咱们可进一步批改之前的权限治理合约,并在合约中依赖Authority接口,当鉴权时,润饰器会调用接口中的形象办法:
contract BasicAuth { Authority public _authority; function setAuthority(Authority authority) public auth { _authority = authority; } modifier isAuthorized() { require(auth(msg.sender, msg.sig), "BasicAuth: only owner or back owner is authorized."); _; } function auth(address src, bytes4 sig) public view returns (bool) { if (src == address(this)) { return true; } else if (src == _owner) { return true; } else if (_authority == Authority(0)) { return false; } else { return _authority.canCall(src, this, sig); } }}
这样,咱们只须要灵便定义实现了canCall接口的合约,在合约的canCall办法中定义具体判断逻辑。而业务合约,例如FruitStore继承BasicAuth合约,在创立时只有传入具体的实现合约,就能够实现不同判断逻辑。
正当预留事件
迄今为止,咱们已实现弱小灵便的权限管理机制,只有事后受权的内部账户能力批改合约owner属性。
不过,仅通过上述合约代码,咱们无奈记录和查问批改、调用函数的历史记录和明细信息。而这样的需要在理论业务场景中亘古未有。比方,FruitStore水果店须要通过查问历史库存批改记录,计算出不同节令的滞销与畅销水果。
一种办法是依靠链下保护独立的台账机制。不过,这种办法存在很多问题:放弃链下台账和链上记录统一的老本开销十分高;同时,智能合约面向链上所有参与者凋谢,一旦其余参与者调用了合约函数,相干交易信息就存在不能同步的危险。
针对此类场景,Solidity提供了event语法。event不仅具备可供SDK监听回调的机制,还能用较低的gas老本将事件参数等信息残缺记录、保留到区块中。FISCO BCOS社区中,也有WEBASE-Collect-Bee这样的工具,在预先实现区块历史事件信息的残缺导出。
WEBASE-Collect-Bee工具参考链接如下:
https://webasedoc.readthedocs..._CN/latest/docs/WeBASE-Collect-Bee/index.html
基于上述权限治理合约,咱们能够定义相应的批改权限事件,其余事件以此类推。
event LogSetAuthority (Authority indexed authority, address indexed from);}
接下来,能够调用相应的事件:
function setAuthority(Authority authority) public auth{ _authority = authority; emit LogSetAuthority(authority, msg.sender); }
当setAuthority函数被调用时,会同时触发LogSetAuthority,将事件中定义的Authority合约地址以及调用者地址记录到区块链交易回执中。当通过控制台调用setAuthority办法时,对应事件LogSetAuthority也会被打印进去。
基于WEBASE-Collect-Bee,咱们能够导出所有该函数的历史信息到数据库中。也可基于WEBASE-Collect-Bee进行二次开发,实现简单的数据查问、大数据分析和数据可视化等性能。
遵循平安编程标准
每一门语言都有其相应的编码标准,咱们须要尽可能严格地遵循Solidity官网编程格调指南,使代码更利于浏览、了解和保护,无效地缩小合约的bug数量。
Solidity官网编程格调指南参考链接如下:
https://solidity.readthedocs....
除了编程标准,业界也总结了很多平安编程指南,例如重入破绽、数据结构溢出、随机数误区、构造函数失控、为初始化的存储指针等等。器重和防备此类危险,采纳业界举荐的平安编程标准至关重要,例如Solidity官网平安编程指南。参考链接如下:
https://solidity.readthedocs....
同时,在合约公布上线后,还须要留神关注、订阅Solidity社区内平安组织或机构公布的各类安全漏洞、攻打手法,一旦呈现问题,及时做到亡羊补牢。
对于重要的智能合约,有必要引入审计。现有的审计包含了人工审计、机器审计等办法,通过代码剖析、规定验证、语义验证和形式化验证等办法保障合约安全性。
尽管本文通篇都在强调,模块化和重用被严格审查并宽泛验证的智能合约是最佳的实际策略。但在理论开发过程,这种假如过于理想化,每个我的项目或多或少都会引入新的代码,甚至从零开始。
不过,咱们依然能够视代码的复用水平进行审计分级,显式地标注出援用的代码,将审计和查看的重点放在新代码上,以节俭审计老本。
最初,“前事不忘后事之师”,咱们须要一直总结和学习前人的最佳实际,动静和可继续地晋升编码工程程度,并一直利用到具体实际中。
积攒和复用成熟的代码
前文面向接口编程中的思维可升高代码耦合,使合约更容易扩大、利于保护。在遵循这条规定之外,还有另外一条忠告:尽可能地复用现有代码库。
智能合约公布后难以批改或撤回,而且公布到公开通明的区块链环境上,就意味着一旦呈现bug造成的损失和危险更甚于传统软件。因而,复用一些更好更平安的轮子远胜过从新造轮子。
在开源社区中,曾经存在大量的业务合约和库可供使用,例如OpenZeppelin等优良的库。
如果在开源世界和过来团队的代码库里找不到适合的可复用代码,倡议在编写新代码时尽可能地测试和欠缺代码设计。此外,还要定期剖析和审查历史合约代码,将其模板化,以便于扩大和复用。
例如,针对下面的BasicAuth,参考防火墙经典的ACL(Access Control List)设计,咱们能够进一步地继承和扩大BasicAuth,形象出ACL合约管制的实现。
contract AclGuard is BasicAuth { bytes4 constant public ANY_SIG = bytes4(uint(-1)); address constant public ANY_ADDRESS = address(bytes20(uint(-1))); mapping (address => mapping (address => mapping (bytes4 => bool))) _acl; function canCall( address src, address dst, bytes4 sig) public view returns (bool) { return _acl[src][dst][sig] || _acl[src][dst][ANY_SIG] || _acl[src][ANY_ADDRESS][sig] || _acl[src][ANY_ADDRESS][ANY_SIG] || _acl[ANY_ADDRESS][dst][sig] || _acl[ANY_ADDRESS][dst][ANY_SIG] || _acl[ANY_ADDRESS][ANY_ADDRESS][sig] || _acl[ANY_ADDRESS][ANY_ADDRESS][ANY_SIG]; } function permit(address src, address dst, bytes4 sig) public onlyAuthorized { _acl[src][dst][sig] = true; emit LogPermit(src, dst, sig); } function forbid(address src, address dst, bytes4 sig) public onlyAuthorized { _acl[src][dst][sig] = false; emit LogForbid(src, dst, sig); } function permit(address src, address dst, string sig) external { permit(src, dst, bytes4(keccak256(sig))); } function forbid(address src, address dst, string sig) external { forbid(src, dst, bytes4(keccak256(sig))); } function permitAny(address src, address dst) external { permit(src, dst, ANY_SIG); } function forbidAny(address src, address dst) external { forbid(src, dst, ANY_SIG); }}
在这个合约里,有调用者地址、被调用合约地址和函数签名三个主要参数。通过配置ACL的拜访策略,能够准确地定义和管制函数拜访行为及权限。合约内置了ANY的常量,匹配任意函数,使拜访粒度的管制更加便捷。这个模板合约实现了弱小灵便的性能,足以满足所有相似权限管制场景的需要。
晋升存储和计算的效率
迄今为止,在上述的推演过程中,更多的是对智能合约编程做加法。但相比传统软件环境,智能合约上的存储和计算资源更加贵重。因而,如何对合约做减法也是用好Solidity的必修课程之一。
选取适合的变量类型
显式的问题可通过EVM编译器检测进去并报错;但大量的性能问题可能被暗藏在代码的细节中。
Solidity提供了十分多准确的根底类型,这与传统的编程语言天壤之别。上面有几个对于Solidity根底类型的小技巧。
在C语言中,能够用short\int\long按需定义整数类型,而到了Solidity,不仅辨别int和uint,甚至还能定义uint的长度,比方uint8是一个字节,uint256是32个字节。这种设计告诫咱们,能用uint8搞定的,相对不要用uint16!
简直所有Solidity的根本类型,都能在申明时指定其大小。开发者肯定要无效利用这一语法个性,编写代码时只有满足需要就尽可能选取小的变量类型。
数据类型bytes32可寄存 32 个(原始)字节,但除非数据是bytes32或bytes16这类定长的数据类型,否则更举荐应用长度能够变动的bytes。bytes相似byte[],但在内部函数中会主动压缩打包,更节俭空间。
如果变量内容是英文的,不须要采纳UTF-8编码,在这里,举荐bytes而不是string。string默认采纳UTF-8编码,所以雷同字符串的存储老本会高很多。
紧凑状态变量打包
除了尽可能应用较小的数据类型来定义变量,有的时候,变量的排列程序也十分重要,可能会影响到程序执行和存储效率。
其中根本原因还是EVM,不论是EVM存储插槽(Storage Slot)还是栈,每个元素长度是一个字(256位,32字节)。
调配存储时,所有变量(除了映射和动静数组等非动态类型)都会按申明程序从地位0开始顺次写下。
在解决状态变量和构造体成员变量时,EVM会将多个元素打包到一个存储插槽中,从而将多个读或写合并到一次对存储的操作中。
值得注意的是,应用小于32 字节的元素时,合约的gas使用量可能高于应用32字节元素时。这是因为EVM每次会操作32个字节,所以如果元素比32字节小,必须应用更多的操作能力将其大小缩减到所需。这也解释了Solidity中最常见的数据类型,例如int,uint,byte32,为何都刚好占用32个字节。
所以,当合约或构造体申明多个状态变量时,是否正当地组合安顿多个存储状态变量和构造体成员变量,使之占用更少的存储地位就非常重要。
例如,在以下两个合约中,通过理论测试,Test1合约比Test2合约占用更少的存储和计算资源。
contract Test1 { //占据2个slot, "gasUsed":188873 struct S { bytes1 b1; bytes31 b31; bytes32 b32; } S s; function f() public { S memory tmp = S("a","b","c"); s = tmp; }}contract Test2 { //占据1个slot, "gasUsed":188937 struct S { bytes31 b31; bytes32 b32; bytes1 b1; } // ……}
优化查问接口
查问接口的优化点很多,比方肯定要在只负责查问的函数申明中增加view修饰符,否则查问函数会被当成交易打包并发送到共识队列,被全网执行并被记录在区块中;这将大大增加区块链的累赘,占用贵重的链上资源。
再如,不要在智能合约中增加简单的查问逻辑,因为任何简单查问代码都会使整个合约变得更长更简单。读者可应用上文提及的WeBASE数据导出组件,将链上数据导出到数据库中,在链下进行查问和剖析。
缩减合约binary长度
开发者编写的Solidity代码会被编译为binary code,而部署智能合约的过程实际上就是通过一个transaction将binary code存储在链上,并获得专属于该合约的地址。
缩减binary code的长度可节俭网络传输、共识打包数据存储的开销。例如,在典型的存证业务场景中,每次客户存证都会新建一个存证合约,因而,该当尽可能地缩减binary code的长度。
常见思路是裁剪不必要的逻辑,删掉冗余代码。特地是在复用代码时,可能引入一些非刚需代码。以上文ACL合约为例,反对管制合约函数粒度的权限。
function canCall( address src, address dst, bytes4 sig ) public view returns (bool) { return _acl[src][dst][sig] || _acl[src][dst][ANY_SIG] || _acl[src][ANY_ADDRESS][sig] || _acl[src][ANY_ADDRESS][ANY_SIG] || _acl[ANY_ADDRESS][dst][sig] || _acl[ANY_ADDRESS][dst][ANY_SIG] || _acl[ANY_ADDRESS][ANY_ADDRESS][sig] || _acl[ANY_ADDRESS][ANY_ADDRESS][ANY_SIG]; }
但在具体业务场景中,只须要管制合约访问者即可,通过删除相应代码,进一步简化应用逻辑。这样一来,对应合约的binary code长度会大大放大。
function canCall( address src, address dst) public view returns (bool) { return _acl[src][dst] || _acl[src][ANY_ADDRESS] || _acl[ANY_ADDRESS][dst]; }
另一种缩减binary code的思路是采纳更紧凑的写法。
经实测,采取如上短路准则的判断语句,其binary长度会比采纳if-else语法的更短。同样,采纳if-else的构造,也会比if-if-if的构造生成更短的binary code。
最初,在对binary code长度有极致要求的场景中,该当尽可能防止在合约中新建合约,这会显著减少binary的长度。例如,某个合约中有如下的构造函数:
constructor() public { // 在结构器内新建一个新对象 _a = new A();}
咱们能够采纳在链下结构A对象,并基于address传输和固定校验的形式,来躲避这一问题。
constructor(address a) public { A _a = A(a); require(_a._owner == address(this));}
当然,这样也可能会使合约交互方式变得复杂。但其提供了无效缩短binary code长度的捷径,须要在具体业务场景中做衡量取舍。
保障合约可降级
经典的三层构造
通过前文形式,咱们尽最大致力放弃合约设计的灵活性;翻箱倒柜复用了轮子;也对公布合约进行全方位、无死角的测试。除此之外,随着业务需要变动,咱们还将面临一个问题:如何保障合约平滑、顺利的降级?
作为一门高级编程语言,Solidity反对运行一些简单管制和计算逻辑,也反对存储智能合约运行后的状态和业务数据。不同于WEB开发等场景的利用-数据库分层架构,Solidity语言甚至没有形象出一层独立的数据存储构造,数据都被保留到了合约中。
然而,一旦合约须要降级,这种模式就会呈现瓶颈。
在Solidity中,一旦合约部署公布后,其代码就无奈被批改,只能通过公布新合约去改变代码。如果数据存储在老合约,就会呈现所谓的“孤儿数据”问题,新合约将失落之前运行的历史业务数据。
这种状况,开发者能够思考将老合约数据迁徙到新合约中,但此操作至多存在两个问题:
- 迁徙数据会减轻区块链的累赘,产生资源节约和耗费,甚至引入平安问题;
- 牵一发而动全身,会引入额定的迁徙数据逻辑,减少合约复杂度。
一种更正当的形式是形象一层独立的合约存储层。这个存储层只提供合约读写的最根本办法,而不蕴含任何业务逻辑。
在这种模式中,存在三种合约角色:
- 数据合约:在合约中保留数据,并提供数据的操作接口。
- 治理合约:设置管制权限,保障只有管制合约才有权限批改数据合约。
- 管制合约:真正须要对数据发动操作的合约。
具体的代码示例如下:
数据合约:
contract FruitStore is BasicAuth { address _latestVersion; mapping(bytes => uint) _fruitStock; modifier onlyLatestVersion() { require(msg.sender == _latestVersion); _; } function upgradeVersion(address newVersion) public { require(msg.sender == _owner); _latestVersion = newVersion; } function setFruitStock(bytes fruit, uint stock) onlyLatestVersion external { _fruitStock[fruit] = stock; }}
治理合约:
contract Admin is BasicAuth { function upgradeContract(FruitStore fruitStore, address newController) isAuthorized external { fruitStore.upgradeVersion(newController); }}
管制合约:
contract FruitStoreController is BasicAuth { function upgradeStock(bytes fruit, uint stock) isAuthorized external { fruitStore.setFruitStock(fruit, stock); }}
一旦函数的管制逻辑须要变更,开发者只需批改FruitStoreController管制合约逻辑,部署一个新合约,而后应用治理合约Admin批改新的合约地址参数就可轻松实现合约降级。这种办法可打消合约降级中因业务管制逻辑扭转而导致的数据迁徙隐患。
但天下没有收费的午餐,这种操作须要在可扩展性和复杂性之间须要做根本的衡量。首先,数据和逻辑的拆散升高了运行性能。其次,进一步封装减少了程序复杂度。最初,越是简单的合约越会减少潜在攻击面,简略的合约比简单的合约更平安。
通用数据结构
到目前为止,还存在一个问题,如果数据合约中的数据结构自身须要降级怎么办?
例如,在FruitStore中,本来只保留了库存信息,当初因为水果销售店生意发展壮大,一共开了十家分店,须要记录每家分店、每种水果的库存和售出信息。
在这种状况下,一种解决方案是采纳内部关联治理形式:创立一个新的ChainStore合约,在这个合约中创立一个mapping,建设分店名和FruitStore的关系。
此外,不同分店须要创立一个FruitStore的合约。为了记录新增的售出信息等数据,咱们还须要新建一个合约来治理。
如果在FruitStore中可预设一些不同类型的reserved字段,可帮忙躲避新建售出信息合约的开销,依然复用FruitStore合约。但这种形式在最开始会减少存储开销。
一种更好的思路是形象一层更为底层和通用的存储构造。
代码如下:
contract commonDB is BasicAuth { mapping(bytes => uint) _uintMapping; function getUint(bytes key) external view returns(uint) { return _uintMapping[key]; } function setUint(bytes key, uint value) isAuthorized onlyLatestVersion external { _uintMapping[key] = value; }}
相似的,咱们可退出所有数据类型变量,帮忙commonDB应答和满足不同的数据类型存储需要。
相应的管制合约可批改如下:
contract FruitStoreControllerV2 is BasicAuth { function upgradeStock(bytes32 storeName, bytes32 fruit, uint stock) isAuthorized external { commonDB.setUint(sha256(storeName, fruit), stock); uint result = commonDB.getUint(sha256(storeName, fruit)); }}
应用以上存储的设计模式,可显著晋升合约数据存储灵活性,保障合约可降级。
家喻户晓,Solidity既不反对数据库,应用代码作为存储entity,也无奈提供更改schema的灵活性。然而,通过这种KV设计,能够使存储自身取得弱小的可扩展性。
总之,没有一个策略是完满的,优良的架构师长于衡量。智能合约设计者须要充沛理解各种计划的利弊,并基于理论状况抉择适合的设计方案。
总结
文至于此,心愿激发读者对在Solidity世界生存与进化的趣味。“若有完满,必有谎话”,软件开发的世界没有银弹。本文行文过程就是从最简略的合约逐步完善和进化的过程。
在Solidity编程世界中,生存与进化都离不开三个关键词:平安、可复用、高效。生命不息,进化不止。短短一篇小文难以穷尽所有生存进化之术,心愿这三个关键词能帮忙大家在Solidity的世界里飞翔畅游,并一直书写辉煌的故事和传说:)
下期预报
FISCO BCOS的代码齐全开源且收费
下载地址:https://github.com/FISCO-BCOS...