智能合约的升级
目前智能合约不支持传统的migration方式,链上的数据与合约的地址紧密地绑定在一起。
这种方式给智能合约的升级带来了相应的困难。
智能合约的外部调用
目前主流的智能合约的升级基于智能合约具备外部调用的能力。
pragma solidity ^0.4.18;
contract C {
uint public data;
address public callAddress;
uint[10] intArray;// size must be fixed
mapping (address => uint) public campaigns;
function getIntArray() constant returns (uint[10])
{
return intArray;
}
// Internal type can not be returned.
// function getMapData() constant returns( mapping (address => uint)){
// return campaigns;
// }
function f(uint a) external returns(uint b){
data = a+2;
return a+2;
}
function f(uint a,address caller) external returns(uint b) {
data = a+2;
callAddress = caller;
return a + 2;
}
function setData(uint a) external {
intArray[0]=12;
intArray[1]=13;
intArray[2]=14;
intArray[3]=15;
intArray[4]=16;
intArray[5]=12;
data = a;
}
//for external call
function getData() external returns(uint) { return data; }
function compute(uint a, uint b) external returns (uint) { return a+b; }
}
contract D {
C c;
address public cAddr ;
uint public local = 234;
function setAddress(address cAddress) public{
c = C(cAddress);
cAddr = cAddress;
}
function callF() public {
local = c.f(7,msg.sender);
}
function callFSingle() public{
local = c.f(8);
}
function setCData() public{
c.setData(3);
local = c.getData();
}
function useCcompute(uint a,uint b) public{
local = c.compute(a,b);
}
}
上述例子中,C智能合约提供了基本的数据类型和围绕数据类型的简单接口,
在D智能合约中保存C智能合约的地址,并调用C智能合约中的读写接口。
需要说明的是:用户A用自己的钱包在D中调用C智能合约,在C中检测msg.sender,发现是C的智能合约中的地址。
如果C智能合约涉及到数据的权限写入,需要在智能合约中做权限的认证。
权限验证暂时的解决方法:函数重载和命名空间智能合约的验证。
解决方法:
数据智能合约的基本数据结构的读写访问智能通过拥有相关证书的人访问,在数据智能合约中所有的写的实现均具有重载函数,只有通过当前版本的控制器合约的验证或者数据智能合约的验证才能够写入数据。
命名空间智能合约作为存储智能合约的地址的核心,和其它的智能合约多次交互,存储最核心的内容。类似于k8s的etcd。
智能合约升级最佳实践参考文档:
http://www.qukuaiwang.com.cn/news/3451.html
设计
以某银行的业务为例:
代理控制器合约:面向Dapp,是所有业务合约的入口,提供命名空间服务,提供了命名空间到合约地址的映射。使得Dapp对链上合约升级导致的地址变更无感知。例如,Dapp对A银行的存款请求只需要(“BankA",deposit,args) 即可。对B银行的取款请求只需要(”BankB",withdraw,args)即可。代理器控制合约实现上应该是区块链底层内置的、固化的,或者是业务上极少变更的。Dapp在业务运行之前已经明确知道代理控制器合约的地址。
命名控制器合约:面向链上合约,提供命名空间服务,提供了命名空间到合约地址的映射。使得链上合约可以在运行时根据命名获得实际的合约地址。例如,A银行控制器合约向命名控制器合约请求(“BankA-Data"),可以获得A银行数据合约地址,使得A银行控制器合约可以在运行时访问A银行数据合约。它与代理控制器合约的主要不同在于服务对象的不同,代理控制器合约面向Dapp,命名控制器合约面向链上合约。另外,命名控制器合约包含有版本控制的设计(下文第3.2节介绍),可以根据需要配合灰度策略的实施。
业务A控制器合约:提供了存款服务接口deposit。部署初始化时已经明确知道自己的身份”BankA"。运行时通过命名控制合约获得”BankA-Data“的合约地址。
A银行数据合约:保存了A银行的当前余额。提供add和sub接口给A银行控制器合约来更新余额信息。
对A银行的存款请求的流程是这样的:
Dapp指定代理控制器合约地址,请求存款交易(“BankA",deposit,money)
代理控制器合约,运行时得到”BankA"对应的A银行控制器合约地址,并向A银行控制器合约请求存款交易(deposit,money)
A银行控制器合约的deposit接口向命名控制器合约请求“BankA-Data"获得A银行数据数据合约地址,并访问到A银行数据合约的数据,然后执行一些存款业务逻辑。返回结果。
依次返回结果到Dapp。
智能合约的升级
智能合约的升级有三种情形:
控制器智能合约的升级不涉及到数据的变更,因此可以直接升级,设置代理控制器合约中的跳转地址,设置命名空间控制器合约中的最新版本的地址。
数据智能合约的升级涉及到数据的变更,首先需要更新命名控制器智能合约中的地址。其次考虑数据迁移的问题。
数据迁移
数据迁移有三种办法:
硬编码迁移法
硬编码迁移法指的是,新版本的数据合约中保存一个指向旧版本数据合约的合约地址,新版本数据合约保存的是增量的数据内容。
这样相当于新版本合约保留了一份旧版本数据的指针,当新版本需要使用旧数据的时候,直接调用旧数据合约地址对应数据接口即可。这样,新旧版本数据合约可以并存,即使是在异常情况下,数据被误写到了旧版本合约上,它依然可以被新版本所访问到。
这个方法的优点是:新旧合约可以同时并存,不增加区块链存储压力,简单灵活,较强的升级容错能力。缺点:持续不断的版本升级会导致形成较长的链式逻辑关系,维护成本较高。
硬拷贝迁移法
硬拷贝迁移法指的是,新版本和旧版本之间切断逻辑关系,利用外部迁移工具,将旧版本数据逐步拷贝到链下,再从链下重新存储到新版本合约的过程。
这个方法的优点是:无历史包袱。缺点是:大幅度增加区块链存储压力;数据迁移工具需要适配不同的数据合约,开发成本较高;迁移过程需要停止服务,否则容易出现脏数据;数据量大时,耗时长,操作复杂,容易出错,基本无法实操。
默克尔树迁移法
默克尔数迁移法要点如下:
利用智能合约语言的面向对象的继承特性,使得新版本合约存储结构完全兼容旧版本合约存储结构。
利用智能合约在区块链上的storage树原理,使得新版本合约的storeage树直接从旧版本合约上衍生。无需显式的迁移过程。
利用区块链交易的原子性,使得新版本合约的部署、数据迁移、升级,原子完成。
这个方法拥有前面两个方法的所有优点,且简单高效,安全,实操性强。缺点:需要区块链底层功能特性的支持。
目前采取第一种解决方案,且根据请求类型决定访问哪个数据智能合约版本的数据。