WeIdentity 智能合约设计与实现¶
WeIdentity智能合约概述¶
WeIdentity使用基于Solidity的智能合约进行开发。Solidity的智能合约语义上是图灵完备的,该语言支持各种基础类型(Booleans,Integers,Address,Bytes,Enum等)、复杂类型(Struct,Mapping,Array等)、复杂的表达式、控制结构和远程调用,以及接口、继承等面向对象的高级语言特性。Solidity是以太坊和FISCO-BCOS所支持的智能合约语言。
智能合约功能强大,因而真实世界中的复杂商业逻辑和应用可以在区块链上轻松实现。然而,智能合约一旦部署,它会在所有区块链节点上独立重复运行,因此原则上认为,只有各业务方需要进行共识的、逻辑可复用的业务才有必要通过智能合约在链上实现。此外,智能合约发布之后,若出现问题需要修复或者业务逻辑变更,是无法通过简单地在原有合约基础上修改再重新发布来解决的。因此,在设计之初还需要结合业务场景思考合适的合约更新机制。总体上,WeIdentity合约的设计原则是:功能完备、逻辑清晰、模块解耦、结构清晰、安全完备、支持升级。
当前,WeIdentity合约层面的工作目标主要包括两部分:
WeIdentity DID智能合约,负责链上ID体系建立,具体包括生成DID(Distributed IDentity)、生成DID Document、DID在链上的读取与更新。
WeIdentity Authority智能合约,负责进行联盟链权限管理,具体包括链上DID角色的定义、操作与权限的定义与控制。
下文将会基于业务目标对这两部分展开描述。
WeIdentity DID智能合约¶
概述¶
从业务视角来看,DID智能合约只需要做一件事,就是如何定义DID Document的存储结构和读写方式。DID Document的结构并不复杂(见规范文档);但在实际的业务中,存在一些挑战:
伴随着接入用户(人与物)的快速增长,DID的总量将会增长迅速,规模庞大。因此,设计一个大而全的映射表是不现实的,这会带来巨大的寻址开销,即使采用传统分库、分表、跨链的思路也难以应付。
DID存在更新的需求。因此,每次都存储完整的Document域在更新情况下会产生大量的历史数据。
因此,WeIdentity使用Linked Event:基于事件链的存储方法来解决以上问题。
存储结构¶
Linked Event的核心实现思路是借助Solidity的事件(Event)机制,采用类似链表的思路对DID Document的更新进行存储和读取。在Solidity里,每个区块都有对应的Event存储区,用于对区块相关的事件进行存储,并最终存入Event log。因此,存储层面上,在不同时间点DID的更新可以存入更新时当前块的Event里,同时将当前块高作为索引记录每次更新事件。读取层面上,如果要读取完整DID Document,只需按索引反向遍历对应的块的Event里即可。基于这一思路,进行以下设计:
设计一个映射记录,使用DID的地址作为索引,用来存储每个DID最近的一次更新事件所对应的块高;
设计一个更新事件,用来记录每次DID更新的相关属性及前一个块高;
设计一个查询函数,用来读取映射记录找到某个DID的最近的块高,以便反向解析具体的更新事件。
以上数据和逻辑会被合并到一个整体合约里。具体流程为:
每当触发一次DID Document的属性更新,就记入一次更新事件,同时记录更新事件所对应的当前块高,存入整体合约的记录映射部分;
记录映射部分存入整体合约的存储区,更新事件最终会存入区块链的Event;
当读取DID Document时,只需通过记录映射读取块高,反向遍历对应的块的Event,解析并找到Document更新相关的事件内容,然后合并即可。
这一流程图可见于:
性能评估¶
使用Linked Event进行存储的优势有以下几点:
非常适合更新的场景。由于Solidity Event的特性,本方案的写性能和存储开销会远远优于完整存储DID Document内容进入合约的解决方案。
更方便的记录历史版本。通过记录每个事件的块高,可以快速的定位到每个事件,在溯源场景下有着广泛的应用;同时,又不需对那些未更新的属性项进行存储。
读性能对更新事件是O(N)的时间增长。因此,在Document更新不频繁的场景下,读性能非常好。由于WeIdentity的DID本身更多地用来存储公钥等信息,更新频率大部分情况下并不高,因此非常适合WeIdentity的使用场景。
WeIdentity Authority智能合约¶
概述¶
Authority智能合约的主要任务是联盟链的权限管理。在WeIdentity的业务场景中,存在以下挑战:
不同的DID实体拥有不同的权限。
例如,存在Authority Issuer这一角色用来描述现实世界中的「权威凭证发行者」,它们能够发行低段位授权CPT,权限高于一般的DID;更进一步地,在Authority Issuer之上存在着委员会(Committee),它们的权限更高,包括了对Authority Issuer的治理等内容。因此,WeIdentity需要设计合理的「角色—操作」二元权限控制。
权限管理的业务逻辑会随着业务迭代而不断更新。
在真实业务场景中,随着业务变化,权限管理逻辑也可能随之改变;同时,不同的业务方可能会有定制化权限管理的需求。因此,WeIdentity需要进行合理的分层设计,将数据和行为逻辑分离,在升级的情况下就只需对行为逻辑部分进行升级,数据存储保持不变,尽可能地降低更新成本。
当前,业内已经有了一些对权限进行操作和维护的开源解决方案,如ds-auth和OpenZepplin的Role智能合约;但它们的权限管理逻辑可扩展性较差且不支持合约分层更新。下文将介绍WeIdentity的Authority智能合约实现。
架构¶
角色与权限¶
当前的WeIdentity角色设计了四种角色:
一般DID。一般的实体(人或物),由WeIdentity的分布式多中心的ID注册机制生成,没有特定权限。
Authority Issuer。授权机构,具有发行低段位授权CPT的权限。
Committee Member。机构委员会成员。具有管理Authority Issuer成员资格的权限。
Administrator。系统管理员。具有管理Committee Member及Authority Issuer成员资格的权限,未来还包括修改合约地址的权限。
每个角色具体的权限表如下:
合约分层¶
WeIdentity采用分层设计模式,即将合约分为逻辑合约、数据合约、及权限合约。
逻辑合约:它专注于数据的逻辑处理和对外提供接口,通过访问数据合约获得数据,对数据做逻辑处理,写回数据合约。一般情况下,控制器合约不需要存储任何数据,它完全依赖外部的输入来决定对数据合约的访问。
数据合约:它专注于数据结构的定义、数据内容的存储和数据读写的直接接口。
权限合约:它专注于判断访问者的角色,并基于判断结果确定不同操作的权限。
上述架构图如下:
权限与安全管理¶
当前的WeIdentity权限管理的挑战是:
合约在链上部署之后,攻击者可能会绕过SDK直接以DApp的形式访问合约。因此合约层面必须要有自完善的权限处理逻辑,不能依赖SDK。
数据合约是公开的,因此数据合约的操作也需要进行权限管理。
WeIdentity的权限管理依赖于一个独立的RoleManager权限管理器合约,它承担了合约所有的权限检查逻辑。WeIdentity的权限粒度是基于角色和操作的二元组,这也是当前大多数智能合约权限控制的通用做法。它的设计要点包括:
将角色和操作权限分别存储。
设计一个权限检查函数checkPermission()供外部调用,输入参数为「地址,操作」的二元组。
对角色和权限分别设计增删改函数供外部调用。
所有WeIdentity的数据合约里需要进行权限检查的操作,都通过外部合约函数调用的方式,调用checkPermission()。
所有WeIdentity依赖权限管理器的合约,需要有更新权限管理器地址的能力。
WeIdentity的权限管理有以下特性:
优秀的可扩展性。WeIdentity的权限控制合约使用外部调用而非继承(如ds-auth和OpenZepplin的Role智能合约实现角色管理方式)方式实现。在权限控制合约升级的场景中,外部调用方案只需简单地将权限管理器合约地址更新即可,极大地提升了灵活度。
使用tx.origin而非msg.sender进行调用源追踪。这是因为用户的权限和自己的DID地址唯一绑定。因此所有权限的验证必须要以最原始用户地址作为判断标准,不能单纯地依赖msg.sender。此外,WeIdentity的权限控制合约需要支持更大的可扩展性,以支持更多公众联盟链的参与成员自行实现不同的Controller。因此,需要通过tx.origin追踪到调用者的WeIdentity DID,并根据DID确定权限。
Specific Issuer(Issuer链上类型声明)¶
WeIdentity支持为每位Authority Issuer在链上声明所属类型,即Specific Issuer。您可以指定某位Authority Issuer的具体类型属性,如学校、政府机构、医院等。当前,此属性与其对应的权限没有直接关系,仅作记录之目的。
WeIdentity Evidence智能合约¶
WeIdentity不仅提供了基于DID的公钥存储 + 数字签名用来防止凭证被篡改,同时也提供了Evidence存证功能,基于区块链不可篡改的特性,为创建出的凭证增信。简单来说,任何使用者,都可以将凭证的内容摘要上传到链上,以便在未来使用时可以根据链上内容比对,以防篡改。内容摘要使用Hash算法,抗逆向反推。
Evidence智能合约包括两个文件:EvidenceFactory工厂合约和Evidence存证模板合约。
工厂合约有以下特性:
通过将存证模板实例化,生成存证合约,并返回存证合约地址。
创建存证时,从入参里,需要指定有哪些WeID是创建出来的存证的特许签名方(Signer)。可有多个签名方。
入参中的dataHash和r,s,v签名值,是存证里需要在链上记录的摘要信息。一旦写入,没有任何办法能够篡改之。
如果dataHash为空(全为0),则会生成一个**空白存证**,可以后续由签名方通过调用addHash接口补上dataHash。
通过模板创建出来的存证合约有以下特性:
签名方在初始创建存证的时候就需要确定,且创建后不能更改。
每个签名方可以通过调用加签名接口(addSignature)对存证增加自己的签名,以增加信用。
签名方可以调用增加额外信息接口(addExtraValue)为存证添加额外信息,及addHash接口为空白存证补上Hash。增加额外信息,增加签名、为空白存证设置Hash,都必须需要Signer之一使用自己的私钥发交易。
r,s,v签名值,可以在创建时由一个签名者上传,也可以由其他签名方在创建完成后后补。如果您使用WeIdentity Java SDK,则会自动生成此签名三项。
添加额外信息、加签名、为空白存证补上Hash,只能由创建时设定好的签名方之一,使用自己的WeID管理的私钥发交易。
一旦一个合约的dataHash或签名被设置为非空值,就没有任何办法可以对其进行二次修改。此时,唯二能够进行的操作就只有增加签名和增加额外信息。
使用合约地址进行管理,保证了前后升级的兼容性,即使更新的版本修改了存证逻辑,对已经上链的存证也不会有影响。
如果一不小心写错了存证信息,就创建一个新存证吧!
WeIdentity CPT智能合约¶
WeIdentity的CPT(Claim Protocol Type)合约,用于在链上存储凭证的Claim模板。CPT合约使用标准的数据-逻辑分离架构。一个数据CPT合约里,最重要的是其jsonSchema部分,它存储了以jsonSchema格式记载的Claim格式内容。区分不同CPT是通过其ID来进行的。
根据CPT使用目的、内容的不同,ID可以被划分成以下三个范围:1~1000(系统CPT),1000~2000000(授权CPT),2000000以上(普通CPT)。
系统CPT表¶
系统CPT的ID落在1~1000里,它们是在WeIdentity智能合约部署之初就创建好的内置CPT,用来完成所有WeIdentity实例的统一功能,它们在部署WeIdentity智能合约时,在初始化过程中部署在链上。系统CPT不支持任何角色创建。
当前,系统CPT表包括以下内容:
关于每个系统CPT的详细字段要求,可以查阅代码中的 对应文件,此处不再详细展开。
授权CPT¶
授权CPT的ID落在1000~2000000里,如Authority合约中所述,授权CPT仅支持由Authority Issuer创建,一般是和具体的联盟链业务相关。
一般CPT¶
一般CPT的ID从2000000开始自增。任何WeID均可以创建此类CPT。