区块链技术在经历了十余年的发展后,渐呈“燎原之势”,不断在各行业落地生根。但同时,从技术的角度看,区块链应用开发仍然有着较高的门槛,存在不少痛点。为了提升应用开发各环节的用户体验,微众银行将自主研发的区块链应用开发组件WeBankBlockchain-SmartDev全面开源,多维度提速区块链应用开发效率。开源公告详见《区块链应用开发组件:助力低代码开发》。
WeBankBlockchain-SmartDev所包含的智能合约库组件,涵盖了从基础类型到上层业务的常见Solidity库代码,用户可根据实际需要进行参考、复用。该智能合约库已经集成到国家信息中心顶层设计的区块链服务网络BSN、微众银行自主研发的区块链中间件平台WeBASE中,并在供应链金融、存证、农牧溯源等多个业务场景中广泛应用。我们期待区块链技术爱好者和开源社区的伙伴们,一同参与共建,协力推动Solidity智能合约库向更成熟的技术、更完善的生态上发展。
智能合约库简介
作为一门实现了图灵完备的智能合约编程语言,Solidity编程语言的开发、设计、迭代、演化的逻辑完全基于区块链,并在区块链领域具有广泛的影响力和详尽的文档,被众多区块链底层平台所支持,其中就包括FISCO BCOS。
但是,Solidity编程语言也存在若干挑战。首先,受区块链昂贵的资源限制,Solidity舍去了诸多在其他语言中常见的特性,例如高级的语法等。其次,流行的Solidity智能合约库多为公有链所开发,与FISCO BCOS存在兼容性的问题。最后,智能合约编程的安全性要求高,且较难对合约进行升级,一旦存在安全漏洞,后果不堪设想。
为了解决上述问题,WeBankBlockchain-SmartDev-Contract智能合约库应运而生,包含了基础类型、数据结构、通用功能、上层业务等智能合约库。用户可根据实际需求进行参考、复用。智能合约库的设计初衷是提供场景化、通用化、可插拔的智能合约代码,从而最大程度地节约开发智能合约的时间,改变智能合约工具库匮乏的局面。
SmartDev-Contract智能合约库是一个“麻雀虽小,五脏俱全”的智能合约的工具类库,通过Solidity的library封装,旨在帮助Solidity开发者提升开发体验,避免重复造轮子,让编写Solidity语言也可以如编写Python语言那样“丝滑顺畅”。
SmartDev-Contract智能合约库的每个合约文件都来自于微众银行区块链工程师的细致打磨,来自于实际使用场景的“聚沙成塔”,覆盖了业务场景开发中的各种“犄角旮旯”,是开发智能合约的“10倍工程师”的不二法门。
从功能上来看,SmartDev-Contract智能合约库涵盖了从基础类型到上层业务的常见代码,用户可根据实际需要进行参考、复用。具体如下:
痛点及解决方式
以在Solidity语言中将address类型转为string为例。
过去:
打开搜索引擎或github->搜索关键字“Solidity address convert to string”->找到相关的搜索结果->拷贝相关的代码->粘贴到自己的智能合约代码中。
如果无法搜索到相关的代码,开发者必须重新造轮子,耗时耗力的同时,还可能引入新的风险。
现在:
直接下载智能合约代码库->解压->找到相关的库合约->引入代码->调用相关函数。智能合约代码库地址:
https://github.com/WeBankBlockchain/SmartDev-Contract/archive/refs/tags/V1.0.0.zip
pragma solidity ^0.4.25;
import "./LibAddress.sol"
contract Demo {
...
address addr = 0xE0f5206BBD039e7b0592d8918820024e2a7437b9;
bytes memory bs = LibAddress.addressToBytes(addr);
}
开发者引入或拷贝网络上未知来源的代码可能出现重大的bug。同样的,自己重新编写的代码可能因为缺乏测试或实践检验,更易出现风险。智能合约库提供了方便、成熟、安全、低成本的解决方案。
痛点一:计算可能溢出
在智能合约的开发中,数值计算问题不可避免。但是,Solidity内置的运算机制不够安全,因计算问题导致的智能合约安全事故屡见不鲜。
SmartDev-Contract智能合约库提供了安全计算的代码类库。以uint256数据类型为例,LibSafeMathForUint256Utils提供了Uint256类型的相关计算操作,且保证数据的正确性和安全性,包括加法、减法、乘法、除法、取模、乘方、最大值、最小值和平均数等操作。其他的数值类型可以自行参考实现。
1、加减乘除运算
function f() public view {
uint256 a = 25;
uint256 b = 20;
// a + b
uint256 c = LibSafeMathForUint256Utils.add(a,b);
// a - b
uint256 d = LibSafeMathForUint256Utils.sub(a,b);
// a * b
uint256 e = LibSafeMathForUint256Utils.mul(a,b);
// a/b
uint256 f = LibSafeMathForUint256Utils.div(a,b);
}
2、取模运算、乘方运算
function f() public view {
uint256 a = 25;
uint256 b = 20;
// a % b
uint256 c = LibSafeMathForUint256Utils.mod(a,b);
// a ^ b
uint256 d = LibSafeMathForUint256Utils.power(a,b);
}
3、最大值、最小值、平均数运算
function f() public view {
uint256 a = 25;
uint256 b = 20;
// max(a, b)
uint256 c= LibSafeMathForUint256Utils.max(a,b);
// min(a, b)
uint256 d = LibSafeMathForUint256Utils.min(a,b);
// average(a, b)
uint256 e = LibSafeMathForUint256Utils.average(a,b);
}
痛点二:转换不够便捷
数值转换工具
基础数据类型转换是编程语言的刚需。LibConverter提供各类Solidity数据基本类型的转换,开发者可以根据此工具扩展为其他的数值转换类型和函数。
1、数值类型向下转换,例如uint256转换为uint8。
function f() public view{
uint256 a = 25;
uint8 b = LibConverter.toUint8(a);
}
2、数值类型转bytes
function f() public view{
uint256 a = 25;
bytes memory b = LibConverter.uintToBytes(a);
}
3、bytes转数值类型
function f() public view{
bytes memory a = "25";
int b = LibConverter.bytesToInt(a);
}
address转换工具
address类型是Solidity特有的数据类型之一。在日常的程序运行逻辑中,常常会涉及到address与bytes和string类型的互转。LibAddress实现了上述的转换功能。
address转bytes
address addr = 0xE0f5206BBD039e7b0592d8918820024e2a7437b9;
bytes memory bs = LibAddress.addressToBytes(addr);
bytes转address
bytes memory bs = newbytes(20);
address addr = LibAddress.bytesToAddress(bs);
address转string
address addr = 0xE0f5206BBD039e7b0592d8918820024e2a7437b9;
string memory addrStr = LibAddress.addressToString(addr);
string转address
string memory str="0xE0f5206BBD039e7b0592d8918820024e2a7437b9";
address addr = LibAddress.stringToAddress(str);
痛点三:数组操作不够丰富
在Solidity中原生支持的数组类型,不支持排序、查找、比较、移除、添加、翻转、合并、去重等众多常用的功能。
SmartDev-Contract智能合约库基于动态数组的结构封装了“LibArrayForUint256Utils”的常用工具函数实现。开发者也可根据自身需要的数据结构,自行封装相关的工具类。
1、添加不重复的元素
uint[] private array;
function f() public view {
array=new uint[](0);
// array add element 2
LibArrayForUint256Utils.addValue(array,2);
// array: {2}
}
2、合并两个数组
uint[] private array1;
uint[] private array1;
function f() public view {
array1=new uint[](2);
array2=new uint[](2);
LibArrayForUint256Utils.extend(array1,array2);
// array1 length 4
}
3、对数组去重
uint[] private array;
function f() public view {
array=new uint[](2);
array[0]=2;
array[1]=2;
LibArrayForUint256Utils.distinct(array);
// array: {2}
}
4、对数组升序排序
uint[] private array;
function f() public view {
array=new uint[](3);
array[0]=3;
array[1]=2;
array[2]=1;
LibArrayForUint256Utils.qsort(array);
// array: {1,2,3}
}
5、二分查找
基于已排序的数组,支持二分查找,提升查找的效率。
uint[] private array;
function f() public view {
array=new uint[](3);
array[0]=3;
array[1]=2;
array[2]=1;
uint256 key=3;
LibArrayForUint256Utils.binarySearch(array,key);
// array: {true, 1}
}
6、删除元素
uint[] private array;
function f() public view {
array=new uint[](3);
array[0]=3;
array[1]=2;
array[2]=1;
LibArrayForUint256Utils.removeByValue(array,2);
// array: {3, 1}
}
痛点四:不提供字符串内置操作
对字符串的操作是开发中较为常见的操作,例如获取字符串长度、大小写转换等。在其他开发语言中,通常会提供一些内置的字符串处理类库。但Solidity本身没有提供字符串内置操作,因此,这部分需求可通过使用SmartDev-Contract智能合约库来满足。
在SmartDev-Contract智能合约库中,对于字符串,我们提供了丰富的功能,这里列举三个比较常见的函数。
1、获取字符串长度
下面的示例中,分别示意了获取字符串长度、字符串字节数:
pragma solidity ^0.4.25;
import "./LibString.sol";
contract Test {
function f() public{
string memory str = "你好";
uint256 lenOfChars = LibString.lenOfChars(str);
uint256 lenOfBytes = LibString.lenOfBytes(str);
require(lenOfChars == 2);
require(lenOfBytes == 6);
}
}
2、大小写转换
下面示例中,将大写转换为小写:
pragma solidity ^0.4.25;
import "./LibString.sol";
contract Test {
function f() public view returns(string memory) {
string memory c = LibString.toUppercase("abcd");// Expected to be ABCD
return c;
}
}
3、相等比较
pragma solidity ^0.4.25;
import "./LibString.sol";
contract Test {
function f() public view {
bool r = LibString.equal("abcd","abcd");//Expected to be true
require(r);
}
}
4、字符串前缀比较
pragma solidity ^0.4.25;
import "./LibString.sol";
contract Test {
function f() public view {
bool r = LibString.startWith("abcd","ab");//Expected to be true
require(r);
}
}
痛点五:高级数据结构不完备
作为一门面向区块链的语言,Solidity为了节省资源,在数据结构层面砍掉了许多特性,这使得和常规语言相比,其在使用上存在较大差异。一方面,Solidity内部仅提供了数组、mapping等数据结构,如果存在其他需求,需自助实现;另一方面,对于mapping,其内部的键仅保存了哈希值,无法获取键的原值。
综上所述,我们在SmartDev-Contract智能合约库中提供了对数据结构的增强支持,以资参考、使用。
Mapping映射
在下面这个例子中,定义了一个Map,然后向里面存放了三个键值对。再通过迭代的方式将key取出,存放在事件里。
pragma solidity ^0.4.25;
import "./LibBytesMap.sol";
contract Test {
using LibBytesMap for LibBytesMap.Map;
LibBytesMap.Map private map;
event Log(bytes key, uint256 index);
function f() public {
string memory k1 = "k1";
string memory k2 = "k2";
string memory k3 = "k3";
string memory v1 = "v1";
string memory v2 = "v2";
string memory v3 = "v3";
map.put(bytes(k1),bytes(v1));
map.put(bytes(k2),bytes(v2));
map.put(bytes(k3),bytes(v3));
// 开始迭代
uint256 i = map.iterate_start();
while(map.can_iterate(i)){
emit Log(map.getKeyByIndex(i), i);
i = map.iterate_next(i);
}
}
}
address set集合
作为多数高级编程语言标配的数据结构,set是一种集合的数据结构,其中每个独特属性的元素都是唯一的。
SmartDev-Contract智能合约库依托动态数组和mapping,实现了一个基础的set集合。此外,由于Solidity不支持泛型机制,开发者可以参考此工具,实现其他元素的set集合。
pragma solidity ^0.4.25;
import "./LibAddressSet.sol";
contract Test {
using LibAddressSet for LibAddressSet.AddressSet;
LibAddressSet.AddressSet private addressSet;
event Log(uint256 size);
function testAddress() public {
//添加元素;
addressSet.add(address(1));
// {1}
// 查询set容器数量
uint256 size = addressSet.getSize();
require(size == 1);
// 获取指定index的元素
address addr = addressSet.get(0);
require(addr == address(1));
// 返回set中所有的元素
addressSet.getAll();
// {0x1}
// 判断元素是否存在
bool contains = addressSet.contains(address(1));
require(contains== true);
// 删除元素
addressSet.remove(address(1));
}
}
除了LibBytesMap和LibAddressSet之外,SmartDev-Contract智能合约库还包含了堆、栈、单向队列、双向队列、双向链表等实用的数据结构。
业务场景的合约模板
针对上层的业务场景,我们选择联盟链落地中最常见、典型的存证和积分业务场景,提供智能合约代码实例。开发者可以基于自身的实际业务场景修改智能合约代码,也可以参考场景中的部分代码,进行扩展和裁剪。
场景一:存证场景
电子数据存证是记录“用户身份验证-数据创建-存储-传输”全过程的方式,应用一系列安全技术全方位确保电子数据的真实性、完整性、安全性,在司法上具备完整的法律效力。
使用区块链智能合约进行数据存在,具有以下优势:
区块链技术完善的防篡改机制:使用区块链技术保全证据,进一步加强了证据不可篡改性。
证据效力得到机构认可:司法机构作为链上节点,对链数据参与认可和签名,事后可从链上确认数据的真实有效性。
服务持续有效:数据被多方共识上链后,即使有部分共识方退出也不会造成数据的丢失或失效。
简要业务流程
在存证场景中可以抽象出三类典型用户:存证方、审核方和取证方。存证方提交需要存证的申请。
审核方基于内容,对存证数据进行审核和签名确认。实际业务场景中,审核方可能会涉及投票和多方审核的多签过程。
当存证上链后,取证方可随时查询存证者地址、时间戳和审核详情等相关信息进行核验。
合约概要设计
首先,代码设计了逻辑和数据层分离。因为Solidity智能合约语言没有独立数据层,为便于合约后续扩展、升级,需要将逻辑和数据层分离,体现在下图中就是将数据层和控制层区分开。
其次,依据合约单一职责原理,代码中引入了权限层。在一条联盟链上所有节点可以自由访问链上数据,智能合约提供了一种修饰器机制,可控制合约给指定授权用户访问。
具体的结构如下:
Authentication:权限合约,用于提供基础的权限控制功能。
EvidenceRepository:存证数据仓库,它继承了权限合约,所有存证数据都被保存到数据合约里。这样可以起到统一存储、统一管理的效果。
RequestRepository:请求数据仓库,存储了存证数据和投票请求信息等。存证方开始提交存证数据并不会直接被写入存证仓库中,而是经过审核方签名完成后才会真正提交,审核方可以为多方。
EvidenceController:控制器,引入了两个数据仓库合约,可以完成所有用户接口的交互。包含了创建存证请求,审核人根据请求进行投票的功能。
场景二:积分场景
区块链积分场景是指多个独立对等的零售商组成联盟,利用公众联盟链为消费者提供真正意义上的全渠道综合消费体验。
在积分场景中引入区块链,具有以下优势:
区块链技术可以增加品牌曝光度:多个机构组成积分联盟,积分可有效通兑,实现客户资源引流,提升营销效果。
保证积分安全性:所有积分的生成和流转过程保存到链上,防止商户篡改和抵赖。
提升用户体验:不同商户和用户之间实现积分流转、互通,更加便利。
简要业务流程
基于区块链技术,多个商家组成积分联盟,实现积分通存通兑、客户资源相互引流等。管理者部署和管理合约。商家有发行积分、拉入其他商家、撤销发行者身份的权限。消费者有开户、销户、消费积分和积分转账的权限。
合约概要设计
在存证合约中,我们引入了数据和逻辑分离的思想;在积分合约中,我们引入管理、数据和逻辑分离的思想。
引入了管理合约后,就实现了类似控制反转的效果,控制合约和数据合约都由管理合约来创建;同时,管理合约还可以随时设置数据合约中控制合约的地址。这样,控制合约就可以随时实现平滑的业务逻辑升级;将管理合约分离出来,还有利于链上权限治理。
Admin:管理并生成合约,控制访问Data合约的地址
Controller:合约层对外暴露服务的控制器
Data:存储业务合约相关的数据
BasicAuth、IssuerRole:权限、角色的工具合约
LibRoles、LibSafeMath:权限mapping,数值计算的库
即刻体验
上述优化及功能所涉及的最新代码和技术文档已同步更新,欢迎体验和 star 支持。如需咨询技术问题,欢迎关注本公众号,对话框回复【小助手】进技术交流群。
文档地址:
https://smartdev-doc.readthedocs.io/zh_CN/latest/docs/WeBankBlockchain-SmartDev-Contract/index.html
GitHub代码库地址:
https://github.com/WeBankBlockchain/SmartDev-Contract
gitee代码库地址:
https://gitee.com/WeBankBlockchain/SmartDev-Contract
欢迎参与WeBankBlockchain的社区建设:
如项目对您有帮助,欢迎点亮我们的小星星(点击项目左上方Star按钮)。
欢迎提交代码(Pull requests)。
提问和提交BUG。
如果发现代码存在安全漏洞,可通过以下地址上报:
https://security.webank.com/