智能合约的一种设计结构

hi 欢迎来到小秘课堂第七期,今天咱们来说讲智能合约的一种设计结构的那些事儿,欢迎主讲人冯开开html

讲师:冯开开git

编辑:Leogithub

前言

智能合约的设计和传统的应用设计有点不一样。传统应用通常为了快速迭代是在产品以后考虑安全,可是 DApp 则须要在产品出来以前就考虑安全问题,它将会关系到帐户资产、用户数据等问题,并且对 DApp 来说,升级是个比较麻烦的事情,所以在智能合约设计时,结构是很是重要的部分。web

目前 DApp 面临问题

首先,是关于 DApp 和 App。事物发展将会遵循技术为王、产品为王、最后到运营为王三个发展阶段。如今,区块链和 DApp 正处于技术为王阶段。整个市场上的 DApp,在性能和用户友好性上,都不如 App。DApp 的优点显而易见:去中心化,它是依附区块链的应用。可是咱们认为不少 DApp 的短板,实际上是由于底层区块链的限制。算法

其次,是关于安全。如今 DApp 爆发的安全漏洞不少,主要缘由是区块链仍处于发展早期。开发 DApp 的基础设施和相关工具都很不成熟,可是黑客是很成熟的,在互联网上久经沙场,对 DApp 世界影响很大。因此,在设计 DApp 时,要了解区块链相关知识,这些是出于安全考虑。 数据库

最后,是关于成本。在以太坊中就是 Gas,部署智能合约将消耗必定 Gas。这是由于 DApp 很消耗 Gas,特别是部署一个大型 DApp(包括后面的维护、升级)。Gas 是什么?是资金。那么,有没有一种结构可以暂时忽略 Gas。这就分红两种方向,一是思考节约 gas 到细微处,用一种怪异不太舒服的写法来节约 Gas;第二种是走向宏观,整个结构是清晰明了的,可是可能会存在浪费 Gas 的行为。 后端

解决办法

第一,是优美结构。一个优美的结构会带来 Gas 的节约,这是我一直相信的。那整个结构是包含哪些方面呢?最宏观的说指分层。这里分层和通常的 app 分层是相通的,好比应用层、逻辑层、数据层。安全

第二,是友好。由于如今一个大型的 DApp,若是有很好的模块化,可能会有十几个智能合约,他们中间可能还有依赖。那就要求你在部署时,须要格外当心。(友好就是说可以一键部署。 )数据结构

第三,是支持升级。这个是在 App 上很常见的,由于咱们在开发一个 App 时,必需要进行版本迭代,新功能的增长,Bug fix...这个就是升级。而咱们知道 DApp 在区块链上,因为区块链的不可篡改性,部署的合约就在那里了。这里面涉及升级的问题就比较复杂。简而言之,咱们这个结构采起了支持升级的方式,升级比较简单的部分是算法部分,算法部分是纯粹的逻辑,替换能够作到无感知,只须要修改逻辑合约的地址便可。比较麻烦的是对数据结构的升级,数据结构涉及到实际数据,若是轻易改动就要兼容之前的数据(这种需求很常见)。数据迁移是一个比较麻烦的事情,在设计数据结构之初尽可能肯定数据结构,避免频繁的变更。数据结构肯定以后,包括 CURD 以及一些 Check 的接口其实也能够肯定。这些能够做为一个最小的数据合约单元。app

第四,是访问控制。要对整个结构中的数据流转进行控制,这是出于安全性的考虑。 

第五,是模块化。一方面,是出于安全考虑,全部东西在一个合约里会增长合约的复杂性,会对安全形成隐患。毕竟复杂是安全的天敌,越简单越安全,这是软件开发中的常识。因此咱们要保证合约的简洁,一眼看过去就知道这个合约是干什么。另外一方面,是保证函数的简单。同样的道理,函数简单意味着组成的合约也是简单的。 

整个分层就是应用层、逻辑层和数据层,这个结构里面的三层,整个 DApp 是偏向后端的,这里三层是智能合约里面的三层。

结构设计

根据以前 hackathon 上作的项目,叫作 summerWar(区块链沙盒游戏),这个项目里的结构基本上遵循这种设计。

summer War GitHub 仓库地址:

https://github.com/CryptapeHackathon/SummerWars

an Arch : layer

这个是咱们整个游戏的结构图,最左边的是 off-chain,是关于链下用户操做。register 是应用层、Permission 是访问控制、后面是逻辑层和数据层,后面是数据流转和调用的图 。

分层:register 是应用层,operator 是单纯逻辑 ,和数据没有关系。后面是数据层,整个数据单独和逻辑拆分。

权限刚才已经阐述了。在整个结构流转中,若是 operator 是操做层的话,用户须要先访问操做层合约,而后由操做层访问底层数据合约。这里限制访问控制,一是指控制各个层之间的调用关系,好比数据类合约严格控制只能让操做类合约来控制,不多是随便的合约就能访问的。这里数据不只是指数据,还包括数据的简单存取接口,至关于一个数据库的概念,包含存储和查询。 二是能够控制具体用户的操做。

An arch: auth

auth 模块实现了上述说的两种访问控制,在结构上是散布在各个层级之间及整个结构与外部用户之间。

对用户进行访问控制是指:假设有 id,就会先检查哪些地址能够操做,这里 id 是指整个 DApp 的身份,和区块链身份不影响,由于区块链因为去中心化缘由是没有身份的。可是开发的 DApp 里面能够定义一个用户名,这个在 DApp 里是可行的,和区块链没有关系。因此这里能够对这个地址进行检查,容许哪些地址进行操做。至于层之间的访问控制,和用户访问控制相似,用户 id 是Dapp 通行的身份,各层合约能够在 register 查询到也能够把这些合约地址做为整个结构中通行的身份。

具体在底层实现要用到两种特性,modifier 和 函数的可见性。经过这两种特性结合可以达到以上效果:某个合约只能某些合约调用,某些合约只能由某些 sender 调用,这样的控制。 

modifier:

https://solidity.readthedocs.io/en/latest/contracts.html#function-modifiers

函数的可见性:

https://solidity.readthedocs.io/en/latest/contracts.html#visibility-and-getters

整个下来,代码的组织结构可能就以下图所示:

App: register

整个结构的分层,咱们先从上往下咱们开始讲。首先是 App 层的 register。regitser 是什么角色?他是注册中心,是一个 hub,咱们指望它可以保存全部 DApp 智能合约的相关信息(地址等),这也是一个用户接入的入口。在这里,除 operator 后续升级和注册外,它至关于一个交互枢纽,能够进行部署、初始化、注册和升级。这里可以实如今 regiter 部署以后,整个 DApp 的智能合约部分也就部署上去了。咱们看一下 register 在整个结构中的位置,是在 off-chain 与 后面操做、数据层之间。

为何要这样作呢?不能直接逻辑层+数据层?这样一个中心化的东西会不会影响整个 DApp 的去中心化?

DApp 去中心化属性是依靠后面区块链实现的,有一个中心化的东西并不影响。它的一个优势是简单。经过一个智能合约可以管理全部模块,这个 register 是不变的,至关于一个不变的点,用来连接各个模块,保证稳定,至关于 DApp 在区块链上一直会有一个稳定的地址长期进行服务。若是,须要支持升级那么不少模块均可改变。然而,若是全部东西能够改变,则会变得很难维护,因此使用 register,可以随时经过这个东西进行查询和操做。还有一个优势是可以节约 Gas。只部署 register,而后就完成了整个 DApp 的部署。是怎么实现的?这里合约能够用 new 来生成其余智能合约。new 并不节约 Gas,节约的是交易相关的消耗。 

Register 包含三类接口

第一类接口:初始化

也就是 Solidity 里面的 constructor,合约的构造函数。它的功能是在部署智能合约时,一次性执行而后销毁。因此初始化时,要存入什么?刚说 register 是一个稳定的东西,那就能把整个结构中,一些相对稳定的东西放在这里初始化。好比多个用户的操做层合约是固定的,而数据层的合约随着用户的注册注销而变化,那么就能够把操做层合约在这里初始化,随着 register 的部署由其进行建立。 

第二类接口:注册

注册是 web 调用,由外部用户来使用的。在初始完以后,至关于整个 DApp 上线了,在用户使用时,可能就有些功能上的注册操做。好比 identity,用户须要注册一个用户名之类。在 register 部署以后,你可以完成初始化的其余操做,类比咱们如今的应用运行以后提供的功能。 

第三类接口:查询

这类接口的使用者分为两类:

  • 外部用户能够查询一些 register 保存的各类模块信息。

  • 当一个合约与其余模块通讯时,它只知道 register 地址,而更多模块合约地址多是经过 register 来生成的随机地址,这个时候就能够经过 register 得到其余模块的地址进行之间的交互。

Logic:Operator

接着往下看是操做层的合约。这里能够作一些模块化的东西,支持升级也是在这里作的,是由于简单。咱们一般讲的支持升级包括两个方面,一个是函数或者接口的升级。另一个是数据的升级或者说迁移。接口的升级比较容易,在区块链上数据升级比较困难,由于数据复制的操做很贵。存数据一字大概 2w gas。咱们这里优先考虑 operator 的升级。

升级有两种方式,对应 evm 的智能合约里面进行交互两种指令,分别有两种升级方式

  • 一个是 call,是消息调用的一种。调用 call 时,至关于把主动权交给另外一个合约了,这个合约在一个新的 evm 执行以后返回一个结果给我。利用这个指令能够完成一个支持升级的方式,就是在 register 作一个相似 router 的东西,记录每个操做类合约的版本号,而后用户就在访问操做类合约的时候选择版本进行下一次的调用,或者 register 帮你转。

  • 第二个是用 delegate call,相对于 call,用 delegate 时主动权并无交出去,整个智能合约代码仍是在我如今的执行环境中执行,这是智能合约库使用的基本指令,不少库的实现都是基于 delegate call 的方式来作的。支持升级就是用一个代理类的合约,用户调用时帮你进行转发。这里会有一个反作用,必须把操做的数据留在 proxy 里面,这是 delegate 的属性。由于这个属性就须要支持升级的合约。有两个要求,一个是纯逻辑的,没有对状态的改变,第二个是没有在对数据留存在外面的要求,没有对数据进行分开的要求,全部版本的数据在 proxy 保存 。

我更推荐前者。后者把数据都存在 proxy 里面,前者是把数据也分开了,更模块化。我我的以为是比较清晰的用法,这里用的也是这个。 

DATA: data

关于数据,这方面的升级其实比较麻烦,会有一些问题。因此咱们设计数据结构时尽可能稳定一点,变更小一些,提早预留好之后要用的字段,避免之后的升级。要升级的话也有两种方法

  • 一种方法是相似于插件的东西,旧的好比是 map 结构,是地址结构体,后面要多加一个字段。那么涉及旧数据怎么办?我能够定义一个插件类的合约,定义一个多余字段加一个指针指回原来的地方,至关于数据分开存。,可是保存一个指针指向旧数据而且可以找到他,可以作一些操做,这样的好处是不会变更数据,可是会增长操做的逻辑,比较复杂,并且不是全部的数据结构都能作的。

  • 第二个是迁移,若是颇有钱的话,能够直接拷贝过来,若是不在意钱这是最简单的方式。

整个结构大概是这样。 

对于升级的一点建议:升级时 copy 数据很贵,因此咱们尽可能避免这样的消耗,前期 gas 消耗也是注意的一个点。第二个是使用库来封装这些逻辑,就是说模块化。尽量逻辑都能成库,能够找比较好的库来用。就是说不少模块交互须要用接口,让合约不依赖模块自己实现而依赖接口,这样保持接口不变的前提下就能升级合约。 

 

关于讲师

冯开开

秘猿科技高级区块链工程师

Github:https://github.com/kaikai1024

秘猿科技 repo:https://github.com/cryptape

链接开发者与运营方的合做平台 CITAHub:https://www.citahub.com/

有任何技术问题能够在论坛讨论:https://talk.nervos.org

相关文章
相关标签/搜索