背景起因
近期开发基于以太坊的智能合约,为实验室的分布式电商系统提供可信的“第三方信用担保”功能。
由于初期项目需求变动会比较频繁,并且智能合约一经发布于区块链上就无法修改,即使智能合约中有Bug需要修复或者业务逻辑变更,它也不能直接在原有的合约上直接修改再重新发布。因此在即将发布第一版之前,需要结合业务场景考虑合理的升级改造机制。
初版的智能合约主要包括如下功能:
1. 买家创建订单,合约记录订单基本信息并且需要买方向合约支付交易费用
2. 卖家取消订单,卖家在发货前可以取消订单,合约自动将交易费用退回给买家
3. 卖家发货,合约记录相应订单的物流信息,修改订单状态
4. 买家确认收货,合约将交易费用转移至卖家,并且修改订单状态为已完成
5. 基本的订单查询功能
通过简单的需求介绍,可以看出合约中会保存着交易费用,订单数据以及基本的电商交易流程动作。
设计方案
起初写的智能合约全部集中在一个contract,里面会保存交易费用,订单数据以及电商交易的全部流程。一旦发布就不能修改(哪怕是添加一个方法,修改方法逻辑以及数据中添加新的字段,,,),因为重新发布新的合约之后,之前的订单数据以及交易费用全部在旧的合约中,从工程的角度来看,同时维护新旧合约(也许好多个旧合约,,,,)简直不能更糟糕。
因此决定将业务逻辑和数据从合约代码层面就做好分离,即合约分为了两类:逻辑合约以及数据合约。逻辑合约通过访问数据合约获得数据,并对数据做逻辑处理,然后写回数据合约,专注于对数据的逻辑处理和对外提供服务。逻辑合约不存储任何状态;数据合约专注于数据结构定义与所存储数据的读写接口。
逻辑合约与数据合约存在操作关系,逻辑上分为四类:
1. 逻辑合约与数据合约 1对1
2. 逻辑合约与数据合约 1对多
3. 逻辑合约与数据合约 多对1
4. 逻辑合约与数据合约 多对多
根据本项目的业务场景,采取逻辑合约与数据合约1对1的关系(后续业务复杂,可以改造为多对1),综上,初期的设计图如下:
实践
首先进行数据与逻辑的分离。
数据合约
数据合约包括状态,以及每个状态对应读写方法(set&&get),部分代码如下:
逻辑合约
逻辑合约因为仅包含业务逻辑,不会存储任何的数据状态,因此使用Solidity的library进行编写。
Solidity的library
library的好处是它是单实例,只会更新一个library文件,不会像contract那样因为各种依赖产生‘涟漪’效应,对于经常变动的业务逻辑代码,使用library每次更新会节省大量gas消耗。
library是需要被合约调用才能执行的,调用library的contract(下面简称A),使用的delegatecall方法,因此library执行的上下文环境在A中,也就是library的msg.sender是A,并且可以直接修改A中的storage。直白说,A调用library,就是把library的代码import到自己的内部执行了。
入口合约
因为业务逻辑合约使用了library编写,library的调用是需要合约触发的,因此需要一个入口合约,来去调用library操作数据合约,而且对于本项目的实际需求,上面将数据以及逻辑进行分离后,忘记了一个重要的方面,就是资金存储,引入入口合约后,正好解决了这个问题,客户端调用入口合约代码,将资金充入入口合约,并且由入口合约进行资金的转移过程。
入口合约调用library来去操作数据合约的数据,因此在入口合约中,需要引入调用的library(或者是library的interface),这样子,如果library添加一个新的方法,不仅library需要更新,入口合约也是需要更新的,而入口合约的作用包括交易资金托管以及调用library方法,更新发生时,需要有适当的方法将旧的入口合约资金转移到新的入口合约中,因此写了kill函数,当更新发生时,将新的合约地址作为参数,执行如下旧合约的kill方法:
Solidity的using关键词
上面反复提到“入口合约调用library来去操作数据合约的数据”,这个地方使用到了using这个Solidiy关键词。
using关键词是Solidity的contract引用library时使用,例子如下:
pragma solidity ^0.4.15;
library SomeLibrary {
function add(uint self, uint b) returns (uint) {
return self+b;
}
}
contract SomeContract {
using SomeLibrary for uint;
function add3(uint number) returns (uint) {
return number.add(3);
}
}
using SomeLibrary for uint 意思是在SomContract合约中,uint类型的参数可以直接调用SomeLibrary的方法,并且将uint自身作为第一个参数传递到library的方法中。
回到项目中,“入口合约调用library来去操作数据合约的数据”,即调用library方法的同时,需要将数据合约作为参数提供给library,因此代码可以写成如下:
在入口合约初始化的时候,将数据合约赋值给tradeData,因为有了using LogicLibrary for address, 因此address类型的tradeData可以直接调用library的方法,并且将其自身(数据合约的地址)传递给了library。
合约间调用的return问题
按照上面的方式改造,在测试过程中,发现当执行查询数据的时候,合约间返回非定长类型的string,会出错,上stackexchange的Ethereum板块中,找到如下解答:
也就是说Solidity对于合约外部调用,返回变长类型的字段,EVM是不支持的。回到项目本身,因为订单数据中定义了多个string类型数据,没有办法去改变数据结构,因此采取折中的办法,对于查询操作,客户端直接读取数据合约(缺点是目前的设计方案数据合约是不可改的,但是对于查询,可以将数据合约中全部状态写好get方法,并由客户端对返回的状态进行整理是可以满足需求的)。
部署
本项目智能合约使用了Truffle进行开发编写,对于升级改造之后的部署脚本,代码如下:
对于Library与Contract的关联,是通过deployer.link实现在字节码级别上的,并不需要合约代码显式写出来。
最终架构
经过上述实践,智能合约成功完成可升级的改造,架构图如下:
后续
经过上面改造,基本满足实验室分布式电商智能合约的升级需求,但是这种方式应用到其他实际场景中,可能存在的问题包括数据合约无法修改;读写操作客户端需要对两个合约进行操作;缺少proxy合约(使得智能合约升级对于客户端dapp无感知)等。所以智能合约的升级改造,还是要和实际需求相结合,此文尽量满足本身项目需求的同时,使用业内通用方法来去实践,若有不足,望批评指正。
参考文章
1. Writing upgradable contracts in Solidity:https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88
2. Solidity’s ‘using’ keyword:https://medium.com/@gus_tavo_guim/soliditys-using-keyword-c05c18aaa088
3. One reason to start using Solidity Libraries:http://blog.ethereum-alarm-clock.com/blog/2015/10/25/one-reason-to-start-using-libraries
出处:http://blog.liuhongnan.com/2017/12/24/智能合约的可升级改造实践/#背景起因
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
-END-
架构文摘
ID:ArchDigest
互联网应用架构丨架构技术丨大型网站丨大数据丨机器学习
更多精彩文章,请点击下方:阅读原文