经典:浅谈以太坊智能合约的设计模式与升级方法

  • 目录
    • 1. 最佳实践
    • 2. 实用设计案例
      • 2.1 控制器合约与数据合约: 1->1
      • 2.2 控制器合约与数据合约: 1->N
      • 2.3 控制器合约与数据合约: N->1
      • 2.4 控制器合约与数据合约: N->N
      • 2.5 总结
    • 3. 升级
      • 3.1 控制器合约升级,数据合约不升级
      • 3.2 控制器合约不升级,数据合约升级
      • 3.3 控制器合约升级,数据合约升级
    • 4. 数据迁移
      • 4.1 硬编码迁移法
      • 4.2 硬拷贝迁移法
      • 4.3 默克尔树迁移法

以太坊EVM是当前区块链行业应用最为普遍的虚拟机。其所支持的智能合约语言是图灵完备的。该语言支持各类基础类型(Booleans,Integers,Address,String,Enum,Address等)、复杂类型(Struct,Mapping,Array等)、复杂的表达式和控制结构及接口继承等面向对象的特性。git

正是因为强大的智能合约语言,本来在真实世界中的复杂商业逻辑和应用都能在区块链上轻松实现。然而须要注意的是,尽管公有链能够实现合理的GAS机制自我保护,联盟链能够用其余机制替代GAS的计算及代币化来保障EVM沙盒安全,但因为区块链运行机制的缘由,智能合约的运行即便是异常运行都会在全部区块链节点上独立重复运行。所以,不管是在公有链仍是联盟链运行智能合约都是很是昂贵(运算资源、存储资源)的操做。github

另外,智能合约与传统应用程序有一个不一样的地方在于智能合约一经发布于区块链上就没法篡改,即便智能合约中有Bug须要修复或者业务逻辑变动,它也不能直接在原有的合约上直接修改再从新发布。所以在设计之初就须要结合业务场景考虑合理的升级机制。安全

总而言之,智能合约实现上要达到的目标是:完备的业务功能、精悍的代码逻辑、良好的模块抽象、清晰的合约结构、合理的安全检查、完备的升级方案。数据结构

智能合约的生命周期主要有设计、开发、部署、运行、升级、销毁。在下文中主要是基于目标在设计阶段、升级阶段的一些梳理总结。架构

1. 最佳实践

从业务视角来看,智能合约只须要作两件事,其一是如何定义数据的结构和读写方式,其二是如何处理数据并对外提供服务接口。app

为了更好的作好模块抽象和合约结构分层,将这两件事分开,既是将业务控制逻辑和数据从合约代码层面就作好分离,这样的处理在复杂业务逻辑场景中通过实践是当前被认为最佳的模式。模块化

这个模式简称为CD(Controller-Data)模式。将合约分为两类:控制器合约(Controller Contract)与数据合约(Data Contract)。工具

CD模式

控制器合约经过访问数据合约得到数据,并对数据作逻辑处理,而后写回数据合约。�它专一于对数据的逻辑处理和对外提供服务。根据处理逻辑的不一样,常见的有命名空间控制器合约、代理控制器合约、业务控制器合约、工厂控制器合约等。通常状况下,控制器合约不须要存储任何数据,它彻底依赖外部的输入来决定对数据合约的访问。特殊状况下,控制器合约能够存储某个固定的数据合约的地址或者命名空间(经过命名空间在运行时得到合约地址)。性能

数据合约专一于数据结构定义与所存储数据的读写裸接口。为了达到数据统一访问管理和数据访问权限控制的目的,最好是将数据读写接口只暴露给对应的控制器合约。禁止其余方式的读写访问。区块链

基于这个模式,遵循从上至下的分析方式,从对外提供的服务接口开始设计各种控制器合约,再逐步过渡到服务接口所须要的数据模型和存储方式,进而设计各种数据合约,能够较为快速的完成合约架构的设计。

2. 实用设计案例

在CD模式下,根据控制器合约与数据合约之间的操做关系,从逻辑上归结为四类:

  1. 控制器合约与数据合约 1->1
  2. 控制器合约与数据合约 1->N
  3. 控制器合约与数据合约 N->1
  4. 控制器合约与数据合约 N->N

假设一个业务场景:将全国全部银行的业务和信息上链。

2.1 控制器合约与数据合约: 1->1

假设全国只有两家银行,A银行和B银行。A银行只有存款业务,B银行只有取款业务。一种可能的设计是这样的:

CD模式

代理控制器合约:面向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“的数据合约“BankA-Data"的地址。

A银行数据合约:保存了A银行的当前余额。提供add和sub接口给A银行控制器合约来更新余额信息。

B银行控制器合约:提供了存款服务接口withdraw。部署初始化时已经明确知道本身的身份”BankB"。而且能够在运行时经过命名控制合约得到”BankB“的数据合约"BankB-Data"的地址。

B银行数据合约:保存了B银行的当前余额。提供add和sub接口给B银行控制器合约来更新余额信息。

对A银行的存款请求的流程是这样的:

  1. Dapp指定代理控制器合约地址,请求存款交易(“BankA",deposit,money)
  2. 代理控制器合约,运行时获得”BankA"对应的A银行控制器合约地址,并向A银行控制器合约请求存款交易(deposit,money)
  3. A银行控制器合约的deposit接口向命名控制器合约请求A银行的数据合约“BankA-Data"的地址,并访问到A银行数据合约的数据,而后执行一些存款业务逻辑。返回结果。
  4. 依次返回结果到Dapp。

2.2 控制器合约与数据合约: 1->N

假设全国有N家银行,全部银行都有存款业务和取款业务,而且业务流程都是同样的。一种可能的设计是这样的:

CD模式

这个设计与上面的2.1不同的地方在于,将存款服务接口和取款接口都集中归结到银行业务控制器合约里面了。这意味着任何银行的存款和取款业务都由银行业务控制器合约来统一处理,处理逻辑上再也不区分是A银行仍是B银行,只是在数据访问的时候须要根据入参的不一样来决定访问不一样的银行数据合约。

还有,于2.1相比,对于Dapp而言,它发出请求的时候只须要将请求发往固定的”Bank"就能够了,不用具体关心某个银行。

另外,因为银行有不少个,而且它们的存储结构都是同样的,所以能够设计一个银行数据合约的工厂控制器合约,来负责对新的数据合约的生成实现模板化。

对A银行的存款请求的流程是这样的:

  1. Dapp指定代理控制器合约地址,请求存款交易(“Bank",deposit,”BankA“,money)
  2. 代理控制器合约,运行时获得”Bank"对应的银行业务控制器合约地址,向银行业务控制器合约请求存款交易(deposit,”BankA“,money)
  3. 银行业务控制器合约的deposit接口向命名控制器合约请求A银行的数据合约“BankA-Data"的地址,并访问到A银行数据合约的数据,而后执行一些存款业务逻辑。返回结果。
  4. 依次返回结果到Dapp。

2.3 控制器合约与数据合约: N->1

假设全国有N家银行,全部银行都有存款业务和取款业务,而且业务流程都是同样的,可是因为业务逻辑较为复杂,出于模块化维护的须要,须要将存款业务和取款业务作分拆。一种可能的设计是这样的:

CD模式

这个设计与上面的2.2不同的地方在于,将存款服务接口和取款接口拆分到了不一样的业务控制器合约里面了。这意味着不一样的业务逻辑从模块上作了清晰的切分。对于Dapp而言,它发出请求的时候须要明确指向所对应的业务接口。

对A银行的存款请求的流程是这样的:

  1. Dapp指定代理控制器合约地址,请求存款交易(“deposit",”BankA“,money)
  2. 代理控制器合约,运行时获得”deposit"对应的存款业务控制器合约地址,向存款业务控制器合约请求存款交易(”BankA“,money)
  3. 存款业务控制器合约的deposit接口向命名控制器合约请求A银行的数据合约“BankA-Data"的地址,并访问到A银行数据合约的数据,而后执行一些存款业务逻辑。返回结果。
  4. 依次返回结果到Dapp。

2.4 控制器合约与数据合约: N->N

此类状况能够拆解为上面三种状况的组合。再也不赘述。

2.5 总结

从Dapp视角考虑,能够总结以下:

CD模式 特色
1->1 面向业务对象
1->N 面向业务流程
N->1 面向业务接口
N->N /

3. 升级

在CD模式下,在业务逻辑变动须要升级合约的状况下,根据控制器合约与数据合约的升级关系来划分,能够概括为如下三种状况:

控制器合约 数据合约
升级 不升级
不升级 升级
升级 升级

在升级过程当中,还须要考虑是全量升级仍是灰度升级?若是是灰度升级,灰度策略是怎么样的?另外,在多链场景和单链场景、跨链场景,升级过程是否有不一样?多链场景的灰度策略如何考虑?新旧版本数据可否共存?若是须要数据迁移,如何作到无缝迁移?

下面以最为常见的1->N 场景来介绍不一样的升级状况。

3.1 控制器合约升级,数据合约不升级

CD模式

如上图所示,银行业务控制器合约从V1升级到V2,而其余的合约和接口都是不须要更新的,假设V2版本相对V1版本只是升级withdraw这个接口。

此时,V2版本的银行业务控制器合约须要作的事情是:

  1. 继承V1版本的银行业务控制器合约。
  2. 增长一个指向V1版本的链上合约地址的成员变量。
  3. 增长一个withdraw开关接口,容许外部帐户经过普通交易来操做V2版本合约的启停灰度策略。
  4. 重载withdraw接口。升级对应的接口逻辑。而且在业务逻辑真正开始执行以前,自定义实现灰度策略(譬如灰度特定用户,或者必定比例用户或者其余策略)。而且须要注意的是在打开灰度开关的状况下,若是请求没有命中灰度策略,则直接透传参数调用V1版本的合约接口,V2版本的withdraw接口不作任何额外工做。

完成V2版本的合约工做以后,便可发布一个普通交易,交易中的逻辑是,先部署V2版本的银行业务控制器合约,再将其地址更新到代理控制器合约中,使得将“Bank”映射到V2版本的合约地址上。这样控制器合约即升级完成。

若是须要回退版本,只须要发布一个普通交易,将代理控制器合约的“Bank”映射到V1版本的合约地址上便可。

以上是单链场景的升级方法。若是是多链场景,只需根据业务的须要来判断链与链之间的灰度策略,重复单链场景的升级便可。若是是跨链场景,须要根据跨链两端的具体状况来制定升级方法。

而对于业务发起端的Dapp而言,它是无任何感知的。它对A银行的存款请求与2.2中彻底同样。依旧是以(“Bank",deposit,”BankA“,money)来发出请求。

总结而言,灰度策略定义在新版本的控制器合约中,数据无需迁移,业务无感知,无需中止服务。无缝升级。

3.2 控制器合约不升级,数据合约升级

CD模式

如上图所示,A银行数据合约从V1升级到V2。而其余的合约和接口都是不须要更新,假设V2版本相对V1版本只是增长新的数据字段loan,并假设银行业务控制器合约本来就能支持到V2版本的A银行数据合约(若是是银行业务控制器合约也须要升级则是3.3节的场景,这里不作描述)。

此时,V2版本的A银行数据合约须要作的事情是:

  1. 继承V1版本的银行数据合约。
  2. 增长一个新字段loan。并实现loan相关的数据接口。

须要注意的是,命名控制器合约有以下重要的设计:

  1. 命名控制器合约是经过访问命名数据合约来存储和访问数据的(为了方便描述,图中并无画出来),所以命令控制器合约是能够参考3.1节的方法来升级的。
  2. 命名数据合约保存了name=>mapping(version=>address)的映射表。
  3. 命名数据合约保存了name=>当前有效的version的映射表。
  4. 命名控制器合约提供了对命名数据合约的name进行遍历的接口。
  5. 命名控制器合约提供了对命名数据合约的映射表的变动接口。

所以,完成V2版本的数据合约以后,便可发布一个普通交易,交易中的逻辑是,先部署V2版本的A银行数据合约,并完成V1版本数据合约到V2版本数据合约的数据迁移(数据迁移方法第4节会描述),接着将V2版本数据合约地址注册到命名控制器合约,并更新BankA-Data所映射的当前有效verison=V2。此时已完成了A银行数据合约的V2版本升级。

若是须要回退版本,只须要发布一个普通交易,将命名控制器合约的BankA-Data所映射的当前有效verison=V1便可。

而对于业务发起端的Dapp而言,它是无任何感知的。它对A银行的存款请求与2.2中彻底同样。依旧是以(“Bank",deposit,”BankA“,money)来发出请求。

对于B银行而言,由于B银行数据合约并无执行升级,因此与它相关的业务请求依然是访问的B银行数据合约的V1版本。因此,对于历史旧版本的数据合约,能够根据业务的须要来判断是否须要对历史旧版本执行升级。有些特殊场景下,须要对全部的历史旧版本数据合约进行升级,这时能够利用命名控制器合约的遍历功能,对全部数据合约进行相似的升级。而对于新加入的C银行,它能够直接使用最新版本V2的数据合约,按照正常流程完成部署与注册,无任何额外操做。

正是因为有了命名控制器合约的版本控制逻辑,可使得即便存在新老版本数据合约并存的状况下,业务控制器类合约依然能正常运行。而对于因为业务的发展和不断的版本升级,会带来命名数据合约的存储量膨胀,致使可能出现的性能降低的状况,依然能够套用本节所述的数据迁移与升级的方法来解决。

以上是单链场景的升级方法。若是是多链场景,只需根据业务的须要来判断链与链之间的灰度策略,重复单链场景的升级便可。若是是跨链场景,须要根据跨链两端的具体状况来制定升级方法。

总结而言,得益于命名控制器合约的版本控制设计,灰度策略能够交给业务方很是自由地选择,业务无感知,无需中止服务。无缝升级。

3.3 控制器合约升级,数据合约升级

此种状况下,实质是3.1与3.2 两种状况的混搭。

所以根据具体状况,拆解成参考3.1和3.2场景方法来执行便可。

4. 数据迁移

如3.2节所描述,在数据合约升级的场景,某些状况须要处理历史数据在新旧合约之间的迁移。迁移的方法有以下三种,各有特色。

4.1 硬编码迁移法

硬编码迁移法指的是,新版本的数据合约中保存一个指向旧版本数据合约的合约地址,新版本数据合约保存的是增量的数据内容。

这样至关于新版本合约保留了一份旧版本数据的指针,当新版本须要使用旧数据的时候,直接调用旧数据合约地址对应数据接口便可。这样,新旧版本数据合约能够并存,即便是在异常状况下,数据被误写到了旧版本合约上,它依然能够被新版本所访问到。

这个方法的优势是:新旧合约能够同时并存,不增长区块链存储压力,简单灵活,较强的升级容错能力。缺点:持续不断的版本升级会致使造成较长的链式逻辑关系,维护成本较高。

4.2 硬拷贝迁移法

硬拷贝迁移法指的是,新版本和旧版本之间切断逻辑关系,利用外部迁移工具,将旧版本数据逐步拷贝到链下,再从链下从新存储到新版本合约的过程。

CD模式

这个方法的优势是:无历史包袱。缺点是:大幅度增长区块链存储压力;数据迁移工具须要适配不一样的数据合约,开发成本较高;迁移过程须要中止服务,不然容易出现脏数据;数据量大时,耗时长,操做复杂,容易出错,基本没法实操。

4.3 默克尔树迁移法

默克尔数迁移法要点以下:

  1. 利用智能合约语言的面向对象的继承特性,使得新版本合约存储结构彻底兼容旧版本合约存储结构。
  2. 利用智能合约在区块链上的storage树原理,使得新版本合约的storeage树直接从旧版本合约上衍生。无需显式的迁移过程。
  3. 利用区块链交易的原子性,使得新版本合约的部署、数据迁移、升级,原子完成。

这个方法拥有前面两个方法的全部优势,且简单高效,安全,实操性强。缺点:须要区块链底层功能特性的支持。

做者:fisco-dev  https://github.com/FISCO-BCOS/Wiki

注:若是看本文的干货头大,那我就夹带个私货推荐一个适合区块链新手的以太坊DApp开发教程:http://t.cn/RnmDmaD

相关文章
相关标签/搜索