FISCO_BCOS

FISCO_BCOS 查看完整档案

深圳编辑清华大学  |  计算机 编辑金链盟  |  运营 编辑 github.com/FISCO-BCOS 编辑
编辑

FISCO BCOS是开源联盟区块链底层技术平台,由金融区块链合作联盟(简称金链盟)成立开源工作组通力打造。成员包括博彦科技、华为、深证通、神州数码、四方精创、腾讯、微众银行、亦笔科技和越秀金科等金链盟成员机构。

代码仓库:https://github.com/FISCO-BCOS

个人动态

FISCO_BCOS 发布了文章 · 9月17日

WeCross Task挑战赛启动,阶梯式跨链任务等你来战

image
WeCross是完全开源的区块链跨链协作平台,FISCO BCOS的开发者们可用该平台实现同异构区块链系统之间的可信互操作。

WeCross一直努力让更多开发者低门槛地用上跨链技术,提供了丰富的学习路径;例如完善的技术文档、举办技术研讨沙龙、打造系列专题解读核心技术模块等等。

“百闻不如一见,百见不如一干”!为了让开发者更高效使用WeCross,WeCross联合FISCO BCOS开源社区推出WeCross Task挑战赛,欢迎各位来一展身手。完成Task可获得相应积分,积分可自由兑换礼品

image

参赛方式

玩法一:参与话题讨论,Show出观点

参与文章《跨链,6个核心接口就够了!》中的话题讨论,小助手将从讨论中精选有益建议、真知灼见等精彩发言给予积分奖励,1条精彩发言可获得10积分,小助手将定期公布。

玩法二:领取Task,Show出代码

我们在WeCross GitHub的 Issue中设置了初阶任务(Task-L)和中阶任务(Task-M)两个梯度的Task,其中标注为help wanted的Issue均为可认领Task。

你可以在Issue中评论“挑战”,认领该Task。同一个Task可以同时被多人认领,每位开发者可同时领取多个Task。

接下来进入最重要的一步——完成代码,提交PR。开发者可自行或组队完成Task,按照PR规范提交代码并合入后,视为挑战成功。如果某个Task被多位开发者认领,则首位成功合入的参赛者视为挑战成功。

  • Task-L (初阶任务),只需了解某个模块一小部分知识并使用较少代码量即可完成,挑战成功获得50积分;
  • Task-M (中阶任务),需要了解某个模块多种知识完成,挑战功获得100积分。

玩法三:火眼金睛,找找找Bug

如果你发现了Bug,可在WeCross GitHub的Issue中按照Issue 规范提交Issue。当你提交的Issue被Reviewer回复“Good Job”时,可获得100积分。

资料包助力玩转WeCross Task挑战赛

我们为大家准备了丰富的学习资料,希望帮助更多对跨链和WeCross感兴趣的开发者轻松上手。更多资料可联系小助手(微信ID:FISCOBCOS010)获取。

1、Gtihub代码仓库
https://github.com/WeBankFinTech/WeCross

2、技术文档
https://fintech.webank.com/developer/docs/wecross/

3、视频:《跨链的东西南北通问题》
https://share.weiyun.com/QAtDZTSM

4、PR规范
https://github.com/WeBankFinTech/WeCross/wiki/贡献代码#pr规范

立刻参与

话不多说,马上参与挑战,Show me the code!解锁丰厚奖品~

添加小助手微信,进群参与讨论

领取Task
https://github.com/WeBankFinTech/WeCross/issues

找找找Bug
https://github.com/WeBankFinTech/WeCross

进群、积分兑换,或参赛过程中遇到任何问题,请随时召唤小助手⬇

image

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 7月30日

智能合约编写之Solidity的编程攻略 | FISCO BCOS超话区块链专场(篇5)

图片1.png

前言


作为一名搬砖多年的资深码农,刚开始接触Solidity便感觉无从下手:昂贵的计算和存储资源、简陋的语法特性、令人抓狂的debug体验、近乎贫瘠的类库支持、一言不合就插入汇编语句……让人不禁怀疑,这都已经过了9012年了,居然还有这种反人类的语言?

对于习惯使用各类日益“傻瓜化”的类库和自动化高级框架的码农而言,学习Solidity的过程就是一场一言难尽的劝退之旅。

但随着对区块链底层技术的深入学习,大家会慢慢理解作为运行在“The World Machine”上的Solidity语言,必须要严格遵循的设计原则以及权衡后必须付出的代价。

正如黑客帝国中那句著名的slogan:“Welcome to the dessert of the real”,在恶劣艰苦的环境面前,最重要的是学会如何适应环境、保存自身并快速进化。

本文总结了一些Solidity编程的攻略,期待各位读者不吝分享交流,达到抛砖引玉之效。

上链的原则


“如无必要,勿增实体”。

基于区块链技术及智能合约发展现状,数据的上链需遵循以下原则:

  • 需要分布式协作的重要数据才上链,非必需数据不上链;
  • 敏感数据脱敏或加密后上链(视数据保密程度选择符合隐私保护安全等级要求的加密算法);
  • 链上验证,链下授权。

如果在智能合约中定义了复杂的逻辑,特别是合约内定义了复杂的函数入参、变量和返回值,就会在编译的时候碰到以下错误:

​Compiler error: Stack too deep, try removing local variables.

这也是社区中的高频技术问题之一。造成这个问题的原因就是EVM所设计用于最大的栈深度为16。

所有的计算都在一个栈内执行,对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端16个元素中的一个到栈顶,或者将栈顶元素和下面16个元素中的一个交换。

所有其他操作都只能取最顶的几个元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。如果一个合约中,入参、返回值、内部变量的大小超过了16个,显然就超出了栈的最大深度。

因此,我们可以使用结构体或数组来封装入参或返回值,达到减少栈顶元素使用的目的,从而避免此错误。

例如以下代码,通过使用bytes数组来封装了原本16个bytes变量。

function doBiz(bytes[] paras) public {
        require(paras.length >= 16);
        // do something
}

保证参数和行为符合预期

心怀“Code is law”的远大理想,极客们设计和创造了区块链的智能合约。

在联盟链中,不同的参与者可以使用智能合约来定义和书写一部分业务或交互的逻辑,以完成部分社会或商业活动。

相比于传统软件开发,智能合约对函数参数和行为的安全性要求更为严格。在联盟链中提供了身份实名和CA证书等机制,可以有效定位和监管所有参与者。不过,智能合约缺乏对漏洞和攻击的事前干预机制。正所谓字字珠玑,如果不严谨地检查智能合约输入参数或行为,有可能会触发一些意想不到的bug。

因此,在编写智能合约时,一定要注意对合约参数和行为的检查,尤其是那些对外部开放的合约函数。

Solidity提供了require、revert、assert等关键字来进行异常的检测和处理。一旦检测并发现错误,整个函数调用会被回滚,所有状态修改都会被回退,就像从未调用过函数一样。

以下分别使用了三个关键字,实现了相同的语义。

require(_data == data, "require data is valid");
​
if(_data != data) { revert("require data is valid"); }
​
assert(_data == data);

不过,这三个关键字一般适用于不同的使用场景:

  • require:最常用的检测关键字,用来验证输入参数和调用函数结果是否合法。
  • revert:适用在某个分支判断的场景下。
  • assert: 检查结果是否正确、合法,一般用于函数结尾。

在一个合约的函数中,可以使用函数修饰器来抽象部分参数和条件的检查。在函数体内,可以对运行状态使用if-else等判断语句进行检查,对异常的分支使用revert回退。在函数运行结束前,可以使用assert对执行结果或中间状态进行断言检查。

在实践中,推荐使用require关键字,并将条件检查移到函数修饰器中去;这样可以让函数的职责更为单一,更专注到业务逻辑中。同时,函数修饰器等条件代码也更容易被复用,合约也会更加安全、层次化。

在本文中,我们以一个水果店库存管理系统为例,设计一个水果超市的合约。这个合约只包含了对店内所有水果品类和库存数量的管理,setFruitStock函数提供了对应水果库存设置的函数。在这个合约中,我们需要检查传入的参数,即水果名称不能为空。

pragma solidity ^0.4.25;
​
contract FruitStore {
    mapping(bytes => uint) _fruitStock;
    modifier validFruitName(bytes fruitName) {
        require(fruitName.length > 0, "fruite name is invalid!");
        _;
    }
    function setFruitStock(bytes fruitName, uint stock) validFruitName(fruitName) external {
        _fruitStock[fruitName] = stock;
    }
}

如上所述,我们添加了函数执行前的参数检查的函数修饰器。同理,通过使用函数执行前和函数执行后检查的函数修饰器,可以保证智能合约更加安全、清晰。智能合约的编写需要设置严格的前置和后置函数检查,来保证其安全性。

严控函数的执行权限


如果说智能合约的参数和行为检测提供了静态的合约安全措施,那么合约权限控制的模式则提供了动态访问行为的控制。

由于智能合约是发布到区块链上,所有数据和函数对所有参与者都是公开透明的,任一节点参与者都可发起交易,无法保证合约的隐私。因此,合约发布者必须对函数设计严格的访问限制机制。

Solidity提供了函数可见性修饰符、修饰器等语法,灵活地使用这些语法,可帮助构建起合法授权、受控调用的智能合约系统。

还是以刚才的水果合约为例。现在getStock提供了查询具体水果库存数量的函数。

pragma solidity ^0.4.25;
​
contract FruitStore {
    mapping(bytes => uint) _fruitStock;
    modifier validFruitName(bytes fruitName) {
        require(fruitName.length > 0, "fruite name is invalid!");
        _;
    }
    function getStock(bytes fruit) external view returns(uint) {
        return _fruitStock[fruit];
    }
    function setFruitStock(bytes fruitName, uint stock) validFruitName(fruitName) external {
        _fruitStock[fruitName] = stock;
    }
}

水果店老板将这个合约发布到了链上。但是,发布之后,setFruitStock函数可被任何其他联盟链的参与者调用。

虽然联盟链的参与者是实名认证且可事后追责;但一旦有恶意攻击者对水果店发起攻击,调用setFruitStock函数就能任意修改水果库存,甚至将所有水果库存清零,这将对水果店正常经营管理产生严重后果。

因此,设置某些预防和授权的措施很必要:对于修改库存的函数setFruitStock,可在函数执行前对调用者进行鉴权。

类似的,这些检查可能会被多个修改数据的函数复用,使用一个onlyOwner的修饰器就可以抽象此检查。_owner字段代表了合约的所有者,会在合约构造函数中被初始化。使用public修饰getter查询函数,就可以通过_owner()函数查询合约的所有者。

contract FruitStore {
    address public  _owner;
    mapping(bytes => uint) _fruitStock;
  
    constructor() public {
        _owner = msg.sender;
    } 
  
    modifier validFruitName(bytes fruitName) {
        require(fruitName.length > 0, "fruite name is invalid!");
        _;
    }
    // 鉴权函数修饰器
    modifier onlyOwner() { 
        require(msg.sender == _owner, "Auth: only owner is authorized.");
        _; 
    }
    function getStock(bytes fruit) external view returns(uint) {
        return _fruitStock[fruit];
    }
    // 添加了onlyOwner修饰器
    function setFruitStock(bytes fruitName, uint stock) 
        onlyOwner validFruitName(fruitName) external {
        _fruitStock[fruitName] = stock;
    }
}

这样一来,我们可以将相应的函数调用权限检查封装到修饰器中,智能合约会自动发起对调用者身份验证检查,并且只允许合约部署者来调用setFruitStock函数,以此保证合约函数向指定调用者开放。

抽象通用的业务逻辑


分析上述FruitStore合约,我们发现合约里似乎混入了奇怪的东西。参考单一职责的编程原则,水果店库存管理合约多了上述函数功能检查的逻辑,使合约无法将所有代码专注在自身业务逻辑中。

对此,我们可以抽象出可复用的功能,利用Solidity的继承机制继承最终抽象的合约。

基于上述FruitStore合约,可抽象出一个BasicAuth合约,此合约包含之前onlyOwner的修饰器和相关功能接口。

contract BasicAuth {
    address public _owner;
​
    constructor() public {
        _owner = msg.sender;
    }
​
    function setOwner(address owner)
        public
        onlyOwner
{
        _owner = owner;
    }
​
    modifier onlyOwner() { 
        require(msg.sender == _owner, "BasicAuth: only owner is authorized.");
        _; 
    }
}

FruitStore可以复用这个修饰器,并将合约代码收敛到自身业务逻辑中。

import "./BasicAuth.sol";
​
contract FruitStore is BasicAuth {
    mapping(bytes => uint) _fruitStock;
​
    function setFruitStock(bytes fruitName, uint stock) 
        onlyOwner validFruitName(fruitName) external {
        _fruitStock[fruitName] = stock;
    }
}

这样一来,FruitStore的逻辑被大大简化,合约代码更精简、聚焦和清晰。

预防私钥的丢失


在区块链中调用合约函数的方式有两种:内部调用和外部调用。

出于隐私保护和权限控制,业务合约会定义一个合约所有者。假设用户A部署了FruitStore合约,那上述合约owner就是部署者A的外部账户地址。这个地址由外部账户的私钥计算生成。

但是,在现实世界中,私钥泄露、丢失的现象比比皆是。一个商用区块链DAPP需要严肃考虑私钥的替换和重置等问题。

这个问题最为简单直观的解决方法是添加一个备用私钥。这个备用私钥可支持权限合约修改owner的操作,代码如下:

contract BasicAuth {
    address public  _owner;
    address public _bakOwner;
    
    constructor(address bakOwner) public {
        _owner = msg.sender;
        _bakOwner = bakOwner;
    }
​
    function setOwner(address owner)
        public
        canSetOwner
{
        _owner = owner;
    }
​
    function setBakOwner(address owner)
        public
        canSetOwner
{
        _bakOwner = owner;
    }
​
    // ...
    
    modifier isAuthorized() { 
        require(msg.sender == _owner || msg.sender == _bakOwner, "BasicAuth: only owner or back owner is authorized.");
        _; 
    }
}

这样,当发现私钥丢失或泄露时,我们可以使用备用外部账户调用setOwner重置账号,恢复、保障业务正常运行。

面向接口编程


上述私钥备份理念值得推崇,不过其具体实现方式存在一定局限性,在很多业务场景下,显得过于简单粗暴。

对于实际的商业场景,私钥的备份和保存需要考虑的维度和因素要复杂得多,对应密钥备份策略也更多元化。

以水果店为例,有的连锁水果店可能希望通过品牌总部来管理私钥,也有的可能通过社交关系重置帐号,还有的可能会绑定一个社交平台的管理帐号……

面向接口编程,而不依赖具体的实现细节,可以有效规避这个问题。例如,我们利用接口功能首先定义一个判断权限的抽象接口:

contract Authority {
    function canCall(
        address src, address dst, bytes4 sig
    ) public view returns (bool);
}

这个canCall函数涵盖了函数调用者地址、目标调用合约的地址和函数签名,函数返回一个bool的结果。这包含了合约鉴权所有必要的参数。

我们可进一步修改之前的权限管理合约,并在合约中依赖Authority接口,当鉴权时,修饰器会调用接口中的抽象方法:

contract BasicAuth {
    Authority  public  _authority;
​
    function setAuthority(Authority authority)
        public
        auth
    {
        _authority = authority;
    }
​
    modifier isAuthorized() { 
        require(auth(msg.sender, msg.sig), "BasicAuth: only owner or back owner is authorized.");
        _; 
    }
    
    function auth(address src, bytes4 sig) public view returns (bool) {
        if (src == address(this)) {
            return true;
        } else if (src == _owner) {
            return true;
        } else if (_authority == Authority(0)) {
            return false;
        } else {
            return _authority.canCall(src, this, sig);
        }
    }
}

这样,我们只需要灵活定义实现了canCall接口的合约,在合约的canCall方法中定义具体判断逻辑。而业务合约,例如FruitStore继承BasicAuth合约,在创建时只要传入具体的实现合约,就可以实现不同判断逻辑。

合理预留事件


迄今为止,我们已实现强大灵活的权限管理机制,只有预先授权的外部账户才能修改合约owner属性。

不过,仅通过上述合约代码,我们无法记录和查询修改、调用函数的历史记录和明细信息。而这样的需求在实际业务场景中比比皆是。比如,FruitStore水果店需要通过查询历史库存修改记录,计算出不同季节的畅销与滞销水果。

一种方法是依托链下维护独立的台账机制。不过,这种方法存在很多问题:保持链下台账和链上记录一致的成本开销非常高;同时,智能合约面向链上所有参与者开放,一旦其他参与者调用了合约函数,相关交易信息就存在不能同步的风险。

针对此类场景,Solidity提供了event语法。event不仅具备可供SDK监听回调的机制,还能用较低的gas成本将事件参数等信息完整记录、保存到区块中。FISCO BCOS社区中,也有WEBASE-Collect-Bee这样的工具,在事后实现区块历史事件信息的完整导出。

WEBASE-Collect-Bee工具参考链接如下:
https://webasedoc.readthedocs..._CN/latest/docs/WeBASE-Collect-Bee/index.html

基于上述权限管理合约,我们可以定义相应的修改权限事件,其他事件以此类推。

event LogSetAuthority (Authority indexed authority, address indexed from);
}

接下来,可以调用相应的事件:

function setAuthority(Authority authority)
        public
        auth
{
        _authority = authority;
        emit LogSetAuthority(authority, msg.sender);
    }

当setAuthority函数被调用时,会同时触发LogSetAuthority,将事件中定义的Authority合约地址以及调用者地址记录到区块链交易回执中。当通过控制台调用setAuthority方法时,对应事件LogSetAuthority也会被打印出来。

基于WEBASE-Collect-Bee,我们可以导出所有该函数的历史信息到数据库中。也可基于WEBASE-Collect-Bee进行二次开发,实现复杂的数据查询、大数据分析和数据可视化等功能。

遵循安全编程规范


每一门语言都有其相应的编码规范,我们需要尽可能严格地遵循Solidity官方编程风格指南,使代码更利于阅读、理解和维护,有效地减少合约的bug数量。

Solidity官方编程风格指南参考链接如下:
https://solidity.readthedocs....

除了编程规范,业界也总结了很多安全编程指南,例如重入漏洞、数据结构溢出、随机数误区、构造函数失控、为初始化的存储指针等等。重视和防范此类风险,采用业界推荐的安全编程规范至关重要,例如Solidity官方安全编程指南。参考链接如下:
https://solidity.readthedocs....

同时,在合约发布上线后,还需要注意关注、订阅Solidity社区内安全组织或机构发布的各类安全漏洞、攻击手法,一旦出现问题,及时做到亡羊补牢。

对于重要的智能合约,有必要引入审计。现有的审计包括了人工审计、机器审计等方法,通过代码分析、规则验证、语义验证和形式化验证等方法保证合约安全性。

虽然本文通篇都在强调,模块化和重用被严格审查并广泛验证的智能合约是最佳的实践策略。但在实际开发过程,这种假设过于理想化,每个项目或多或少都会引入新的代码,甚至从零开始。

不过,我们仍然可以视代码的复用程度进行审计分级,显式地标注出引用的代码,将审计和检查的重点放在新代码上,以节省审计成本。

最后,“前事不忘后事之师”,我们需要不断总结和学习前人的最佳实践,动态和可持续地提升编码工程水平,并不断应用到具体实践中。

积累和复用成熟的代码


前文面向接口编程中的思想可降低代码耦合,使合约更容易扩展、利于维护。在遵循这条规则之外,还有另外一条忠告:尽可能地复用现有代码库。

智能合约发布后难以修改或撤回,而且发布到公开透明的区块链环境上,就意味着一旦出现bug造成的损失和风险更甚于传统软件。因此,复用一些更好更安全的轮子远胜过重新造轮子。

在开源社区中,已经存在大量的业务合约和库可供使用,例如OpenZeppelin等优秀的库。

如果在开源世界和过去团队的代码库里找不到合适的可复用代码,建议在编写新代码时尽可能地测试和完善代码设计。此外,还要定期分析和审查历史合约代码,将其模板化,以便于扩展和复用。

例如,针对上面的BasicAuth,参考防火墙经典的ACL(Access Control List)设计,我们可以进一步地继承和扩展BasicAuth,抽象出ACL合约控制的实现。

​contract AclGuard is BasicAuth {
    bytes4 constant public ANY_SIG = bytes4(uint(-1));
    address constant public ANY_ADDRESS = address(bytes20(uint(-1)));
    mapping (address => mapping (address => mapping (bytes4 => bool))) _acl;
​
    function canCall(
        address src, address dst, bytes4 sig
) public view returns (bool) {
        return _acl[src][dst][sig]
            || _acl[src][dst][ANY_SIG]
            || _acl[src][ANY_ADDRESS][sig]
            || _acl[src][ANY_ADDRESS][ANY_SIG]
            || _acl[ANY_ADDRESS][dst][sig]
            || _acl[ANY_ADDRESS][dst][ANY_SIG]
            || _acl[ANY_ADDRESS][ANY_ADDRESS][sig]
            || _acl[ANY_ADDRESS][ANY_ADDRESS][ANY_SIG];
    }
​
    function permit(address src, address dst, bytes4 sig) public onlyAuthorized {
        _acl[src][dst][sig] = true;
        emit LogPermit(src, dst, sig);
    }
​
    function forbid(address src, address dst, bytes4 sig) public onlyAuthorized {
        _acl[src][dst][sig] = false;
        emit LogForbid(src, dst, sig);
    }
    
    function permit(address src, address dst, string sig) external {
        permit(src, dst, bytes4(keccak256(sig)));
    }
    
    function forbid(address src, address dst, string sig) external {
        forbid(src, dst, bytes4(keccak256(sig)));
    }
​
    function permitAny(address src, address dst) external {
        permit(src, dst, ANY_SIG);
    }
    
    function forbidAny(address src, address dst) external {
        forbid(src, dst, ANY_SIG);
    }
}

在这个合约里,有调用者地址、被调用合约地址和函数签名三个主要参数。通过配置ACL的访问策略,可以精确地定义和控制函数访问行为及权限。合约内置了ANY的常量,匹配任意函数,使访问粒度的控制更加便捷。这个模板合约实现了强大灵活的功能,足以满足所有类似权限控制场景的需求。

提升存储和计算的效率


迄今为止,在上述的推演过程中,更多的是对智能合约编程做加法。但相比传统软件环境,智能合约上的存储和计算资源更加宝贵。因此,如何对合约做减法也是用好Solidity的必修课程之一。

选取合适的变量类型

显式的问题可通过EVM编译器检测出来并报错;但大量的性能问题可能被隐藏在代码的细节中。

Solidity提供了非常多精确的基础类型,这与传统的编程语言大相径庭。下面有几个关于Solidity基础类型的小技巧。

在C语言中,可以用short\int\long按需定义整数类型,而到了Solidity,不仅区分int和uint,甚至还能定义uint的长度,比如uint8是一个字节,uint256是32个字节。这种设计告诫我们,能用uint8搞定的,绝对不要用uint16!

几乎所有Solidity的基本类型,都能在声明时指定其大小。开发者一定要有效利用这一语法特性,编写代码时只要满足需求就尽可能选取小的变量类型。

数据类型bytes32可存放 32 个(原始)字节,但除非数据是bytes32或bytes16这类定长的数据类型,否则更推荐使用长度可以变化的bytes。bytes类似byte[],但在外部函数中会自动压缩打包,更节省空间。

如果变量内容是英文的,不需要采用UTF-8编码,在这里,推荐bytes而不是string。string默认采用UTF-8编码,所以相同字符串的存储成本会高很多。

紧凑状态变量打包

除了尽可能使用较小的数据类型来定义变量,有的时候,变量的排列顺序也非常重要,可能会影响到程序执行和存储效率。

其中根本原因还是EVM,不管是EVM存储插槽(Storage Slot)还是栈,每个元素长度是一个字(256位,32字节)。

分配存储时,所有变量(除了映射和动态数组等非静态类型)都会按声明顺序从位置0开始依次写下。

在处理状态变量和结构体成员变量时,EVM会将多个元素打包到一个存储插槽中,从而将多个读或写合并到一次对存储的操作中。

值得注意的是,使用小于32 字节的元素时,合约的gas使用量可能高于使用32字节元素时。这是因为EVM每次会操作32个字节,所以如果元素比32字节小,必须使用更多的操作才能将其大小缩减到所需。这也解释了Solidity中最常见的数据类型,例如int,uint,byte32,为何都刚好占用32个字节。

所以,当合约或结构体声明多个状态变量时,能否合理地组合安排多个存储状态变量和结构体成员变量,使之占用更少的存储位置就十分重要。

例如,在以下两个合约中,经过实际测试,Test1合约比Test2合约占用更少的存储和计算资源。

contract Test1 {
    //占据2个slot, "gasUsed":188873
    struct S {
        bytes1 b1;
        bytes31 b31;
        bytes32 b32;
    }
    S s;
    function f() public {
        S memory tmp = S("a","b","c");
        s = tmp;
    }
}
​
contract Test2 {
    //占据1个slot, "gasUsed":188937
    struct S {
        bytes31 b31;
        bytes32 b32;
        bytes1 b1;
    }
    // ……
}

优化查询接口

查询接口的优化点很多,比如一定要在只负责查询的函数声明中添加view修饰符,否则查询函数会被当成交易打包并发送到共识队列,被全网执行并被记录在区块中;这将大大增加区块链的负担,占用宝贵的链上资源。

再如,不要在智能合约中添加复杂的查询逻辑,因为任何复杂查询代码都会使整个合约变得更长更复杂。读者可使用上文提及的WeBASE数据导出组件,将链上数据导出到数据库中,在链下进行查询和分析。

缩减合约binary长度

开发者编写的Solidity代码会被编译为binary code,而部署智能合约的过程实际上就是通过一个transaction将binary code存储在链上,并取得专属于该合约的地址。

缩减binary code的长度可节省网络传输、共识打包数据存储的开销。例如,在典型的存证业务场景中,每次客户存证都会新建一个存证合约,因此,应当尽可能地缩减binary code的长度。

常见思路是裁剪不必要的逻辑,删掉冗余代码。特别是在复用代码时,可能引入一些非刚需代码。以上文ACL合约为例,支持控制合约函数粒度的权限。

function canCall(
        address src, address dst, bytes4 sig
    ) public view returns (bool) {
        return _acl[src][dst][sig]
            || _acl[src][dst][ANY_SIG]
            || _acl[src][ANY_ADDRESS][sig]
            || _acl[src][ANY_ADDRESS][ANY_SIG]
            || _acl[ANY_ADDRESS][dst][sig]
            || _acl[ANY_ADDRESS][dst][ANY_SIG]
            || _acl[ANY_ADDRESS][ANY_ADDRESS][sig]
            || _acl[ANY_ADDRESS][ANY_ADDRESS][ANY_SIG];
    }

但在具体业务场景中,只需要控制合约访问者即可,通过删除相应代码,进一步简化使用逻辑。这样一来,对应合约的binary code长度会大大缩小。

function canCall(
        address src, address dst
) public view returns (bool) {
        return _acl[src][dst]
            || _acl[src][ANY_ADDRESS]
            || _acl[ANY_ADDRESS][dst];
    }

另一种缩减binary code的思路是采用更紧凑的写法。

经实测,采取如上短路原则的判断语句,其binary长度会比采用if-else语法的更短。同样,采用if-else的结构,也会比if-if-if的结构生成更短的binary code。

最后,在对binary code长度有极致要求的场景中,应当尽可能避免在合约中新建合约,这会显著增加binary的长度。例如,某个合约中有如下的构造函数:

constructor() public {
        // 在构造器内新建一个新对象
        _a = new A();
}

我们可以采用在链下构造A对象,并基于address传输和固定校验的方式,来规避这一问题。

constructor(address a) public {
        A _a = A(a);
        require(_a._owner == address(this));
}

当然,这样也可能会使合约交互方式变得复杂。但其提供了有效缩短binary code长度的捷径,需要在具体业务场景中做权衡取舍。

保证合约可升级


经典的三层结构

通过前文方式,我们尽最大努力保持合约设计的灵活性;翻箱倒柜复用了轮子;也对发布合约进行全方位、无死角的测试。除此之外,随着业务需求变化,我们还将面临一个问题:如何保证合约平滑、顺利的升级?

作为一门高级编程语言,Solidity支持运行一些复杂控制和计算逻辑,也支持存储智能合约运行后的状态和业务数据。不同于WEB开发等场景的应用-数据库分层架构,Solidity语言甚至没有抽象出一层独立的数据存储结构,数据都被保存到了合约中。

但是,一旦合约需要升级,这种模式就会出现瓶颈。

在Solidity中,一旦合约部署发布后,其代码就无法被修改,只能通过发布新合约去改动代码。假如数据存储在老合约,就会出现所谓的“孤儿数据”问题,新合约将丢失之前运行的历史业务数据。

这种情况,开发者可以考虑将老合约数据迁移到新合约中,但此操作至少存在两个问题:

  1. 迁移数据会加重区块链的负担,产生资源浪费和消耗,甚至引入安全问题;
  2. 牵一发而动全身,会引入额外的迁移数据逻辑,增加合约复杂度。

一种更合理的方式是抽象一层独立的合约存储层。这个存储层只提供合约读写的最基本方法,而不包含任何业务逻辑。

在这种模式中,存在三种合约角色:

  • 数据合约:在合约中保存数据,并提供数据的操作接口。
  • 管理合约:设置控制权限,保证只有控制合约才有权限修改数据合约。
  • 控制合约:真正需要对数据发起操作的合约。

具体的代码示例如下:

数据合约:

contract FruitStore is BasicAuth {
    address _latestVersion; 
    mapping(bytes => uint) _fruitStock;
    
    modifier onlyLatestVersion() {
       require(msg.sender == _latestVersion);
        _;
    }
​
    function upgradeVersion(address newVersion) public {
        require(msg.sender == _owner);
        _latestVersion = newVersion;
    }
    
    function setFruitStock(bytes fruit, uint stock) onlyLatestVersion external {
        _fruitStock[fruit] = stock;
    }
}

管理合约:

contract Admin is BasicAuth {
    function upgradeContract(FruitStore fruitStore, address newController) isAuthorized external {
        fruitStore.upgradeVersion(newController);
    }
}

控制合约:

contract FruitStoreController is BasicAuth {
    function upgradeStock(bytes fruit, uint stock) isAuthorized external {
        fruitStore.setFruitStock(fruit, stock);
    }
}

一旦函数的控制逻辑需要变更,开发者只需修改FruitStoreController控制合约逻辑,部署一个新合约,然后使用管理合约Admin修改新的合约地址参数就可轻松完成合约升级。这种方法可消除合约升级中因业务控制逻辑改变而导致的数据迁移隐患。

但天下没有免费的午餐,这种操作需要在可扩展性和复杂性之间需要做基本的权衡。首先,数据和逻辑的分离降低了运行性能。其次,进一步封装增加了程序复杂度。最后,越是复杂的合约越会增加潜在攻击面,简单的合约比复杂的合约更安全。

通用数据结构

到目前为止,还存在一个问题,假如数据合约中的数据结构本身需要升级怎么办?

例如,在FruitStore中,原本只保存了库存信息,现在由于水果销售店生意发展壮大,一共开了十家分店,需要记录每家分店、每种水果的库存和售出信息。

在这种情况下,一种解决方案是采用外部关联管理方式:创建一个新的ChainStore合约,在这个合约中创建一个mapping,建立分店名和FruitStore的关系。

此外,不同分店需要创建一个FruitStore的合约。为了记录新增的售出信息等数据,我们还需要新建一个合约来管理。

假如在FruitStore中可预设一些不同类型的reserved字段,可帮助规避新建售出信息合约的开销,仍然复用FruitStore合约。但这种方式在最开始会增加存储开销。

一种更好的思路是抽象一层更为底层和通用的存储结构。

代码如下:

contract commonDB  is BasicAuth {
    mapping(bytes => uint) _uintMapping;
    
    function getUint(bytes key) external view returns(uint) {
        return _uintMapping[key];
    }
​
    function setUint(bytes key, uint value) isAuthorized onlyLatestVersion external {
        _uintMapping[key] = value;
    }
​
}

类似的,我们可加入所有数据类型变量,帮助commonDB应对和满足不同的数据类型存储需求。

相应的控制合约可修改如下:

contract FruitStoreControllerV2 is BasicAuth {
    function upgradeStock(bytes32 storeName, bytes32 fruit, uint stock) 
        isAuthorized external {
        commonDB.setUint(sha256(storeName, fruit), stock);
        uint result = commonDB.getUint(sha256(storeName, fruit));
    }
}

使用以上存储的设计模式,可显著提升合约数据存储灵活性,保证合约可升级。

众所周知,Solidity既不支持数据库,使用代码作为存储entity,也无法提供更改schema的灵活性。但是,通过这种KV设计,可以使存储本身获得强大的可扩展性。

总之,没有一个策略是完美的,优秀的架构师善于权衡。智能合约设计者需要充分了解各种方案的利弊,并基于实际情况选择合适的设计方案。

总结


文至于此,希望激起读者对在Solidity世界生存与进化的兴趣。“若有完美,必有谎言”,软件开发的世界没有银弹。本文行文过程就是从最简单的合约逐步完善和进化的过程。

在Solidity编程世界中,生存与进化都离不开三个关键词:安全、可复用、高效。生命不息,进化不止。短短一篇小文难以穷尽所有生存进化之术,希望这三个关键词能帮助大家在Solidity的世界里翱翔畅游,并不断书写辉煌的故事和传说:)

下期预告


图片4.png

FISCO BCOS的代码完全开源且免费
下载地址:https://github.com/FISCO-BCOS...

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 7月29日

智能合约编写之 Solidity的设计模式 | FISCO BCOS超话区块链专场(篇4)

图片1.png

前言


随着区块链技术发展,越来越多的企业与个人开始将区块链与自身业务相结合。

区块链所具有的独特优势,例如,数据公开透明、不可篡改,可以为业务带来便利。但与此同时,也存在一些隐患。数据的公开透明,意味着任何人都可以读取;不可篡改,意味着信息一旦上链就无法删除,甚至合约代码都无法被更改。

除此之外,合约的公开性、回调机制,每一个特点都可被利用,作为攻击手法,稍有不慎,轻则合约形同虚设,重则要面临企业机密泄露的风险。所以,在业务合约上链前,需要预先对合约的安全性、可维护性等方面作充分考虑。

幸运的是,通过近些年Solidity语言的大量实践,开发者们不断提炼和总结,已经形成了一些"设计模式",来指导应对日常开发常见的问题。

智能合约设计模式概述


2019年,IEEE收录了维也纳大学一篇题为《Design Patterns For Smart Contracts In the Ethereum Ecosystem》的论文。这篇论文分析了那些火热的Solidity开源项目,结合以往的研究成果,整理出了18种设计模式。

这些设计模式涵盖了安全性、可维护性、生命周期管理、鉴权等多个方面。

图片2.png

接下来,本文将从这18种设计模式中选择最为通用常见的进行介绍,这些设计模式在实际开发经历中得到了大量检验。

安全性(Security)


智能合约编写,首要考虑的就是安全性问题。

在区块链世界中,恶意代码数不胜数。如果你的合约包含了跨合约调用,就要特别当心,要确认外部调用是否可信,尤其当其逻辑不为你所掌控的时候。

如果缺乏防人之心,那些“居心叵测”的外部代码就可能将你的合约破坏殆尽。比如,外部调用可通过恶意回调,使代码被反复执行,从而破坏合约状态,这种攻击手法就是著名的Reentrance Attack(重放攻击)。

这里,先引入一个重放攻击的小实验,以便让读者了解为什么外部调用可能导致合约被破坏,同时帮助更好地理解即将介绍的两种提升合约安全性的设计模式。

关于重放攻击,这里举个精简的例子。

AddService合约是一个简单的计数器,每个外部合约可以调用AddService合约的addByOne来将字段_count加一,同时通过require来强制要求每个外部合约最多只能调用一次该函数。

这样,_count字段就精确的反应出AddService被多少合约调用过。在addByOne函数的末尾,AddService会调用外部合约的回调函数notify。AddService的代码如下:

contract AddService{
​
    uint private _count;
    mapping(address=>bool) private _adders;
​
    function addByOne() public {
        //强制要求每个地址只能调用一次
        require(_adders[msg.sender] == false, "You have added already");
        //计数
        _count++;
        //调用账户的回调函数
        AdderInterface adder = AdderInterface(msg.sender);
        adder.notify();
        //将地址加入已调用集合
        _adders[msg.sender] = true;   
    }
}
​
contract AdderInterface{
    function notify() public;  
}

如果AddService如此部署,恶意攻击者可以轻易控制AddService中的_count数目,使该计数器完全失效。

攻击者只需要部署一个合约BadAdder,就可通过它来调用AddService,就可以达到攻击效果。BadAdder合约如下:

contract BadAdder is AdderInterface{
​
    AddService private _addService = //...;
    uint private _calls;
​
    //回调
    function notify() public{
        if(_calls > 5){
            return;
        }
        _calls++;
        //Attention !!!!!!
        _addService.addByOne();
    }
​
    function doAdd() public{
        _addService.addByOne();    
    }
}

BadAdder在回调函数notify中,反过来继续调用AddService,由于AddService糟糕的代码设计,require条件检测语句被轻松绕过,攻击者可以直击_count字段,使其被任意地重复添加。

攻击过程的时序图如下:

图片3.png

在这个例子中,AddService难以获知调用者的回调逻辑,但依然轻信了这个外部调用,而攻击者利用了AddService糟糕的代码编排,导致悲剧的发生。

本例子中去除了实际的业务意义,攻击后果仅仅是_count值失真。真正的重放攻击,可对业务造成严重后果。比如在统计投票数目是,投票数会被改得面目全非。

打铁还需自身硬,如果想屏蔽这类攻击,合约需要遵循良好的编码模式,下面将介绍两个可有效解除此类攻击的设计模式。

Checks-Effects-Interaction - 保证状态完整,再做外部调用

该模式是编码风格约束,可有效避免重放攻击。通常情况下,一个函数可能包含三个部分:

  • Checks:参数验证
  • Effects:修改合约状态
  • Interaction:外部交互

这个模式要求合约按照Checks-Effects-Interaction的顺序来组织代码。它的好处在于进行外部调用之前,Checks-Effects已完成合约自身状态所有相关工作,使得状态完整、逻辑自洽,这样外部调用就无法利用不完整的状态进行攻击了。

回顾前文的AddService合约,并没有遵循这个规则,在自身状态没有更新完的情况下去调用了外部代码,外部代码自然可以横插一刀,让_adders[msg.sender]=true永久不被调用,从而使require语句失效。我们以checks-effects-interaction的角度审阅原来的代码:

    //Checks
    require(_adders[msg.sender] == false, "You have added already");
    //Effects    
    _count++;
    //Interaction    
    AdderInterface adder = AdderInterface(msg.sender);
    adder.notify();
    //Effects
    _adders[msg.sender] = true;

只要稍微调整顺序,满足Checks-Effects-Interaction模式,悲剧就得以避免:

    //Checks
    require(_adders[msg.sender] == false, "You have added already");
    //Effects    
    _count++;
    _adders[msg.sender] = true;
    //Interaction    
    AdderInterface adder = AdderInterface(msg.sender);
    adder.notify(); 

由于_adders映射已经修改完毕,当恶意攻击者想递归地调用addByOne,require这道防线就会起到作用,将恶意调用拦截在外。

虽然该模式并非解决重放攻击的唯一方式,但依然推荐开发者遵循。

Mutex - 禁止递归

Mutex模式也是解决重放攻击的有效方式。它通过提供一个简单的修饰符来防止函数被递归调用:

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        //防止递归
        require(!locked, "Reentrancy detected");
        locked = true;
        _;
        locked = false;
    }
​
    //调用该函数将会抛出Reentrancy detected错误
    function some() public noReentrancy{
        some();
    }
}

在这个例子中,调用some函数前会先运行noReentrancy修饰符,将locked变量赋值为true。如果此时又递归地调用了some,修饰符的逻辑会再次激活,由于此时的locked属性已为true,修饰符的第一行代码会抛出错误。

可维护性(Maintaince)


在区块链中,合约一旦部署,就无法更改。当合约出现了bug,通常要面对以下问题:

  1. 合约上已有的业务数据怎么处理?
  2. 怎么尽可能减少升级影响范围,让其余功能不受影响?
  3. 依赖它的其他合约该怎么办?

回顾面向对象编程,其核心思想是将变化的事物和不变的事物相分离,以阻隔变化在系统中的传播。所以,设计良好的代码通常都组织得高度模块化、高内聚低耦合。利用这个经典的思想可解决上面的问题。

Data segregation - 数据与逻辑相分离

了解该设计模式之前,先看看下面这个合约代码:

contract Computer{
​
    uint private _data;
​
    function setData(uint data) public {
        _data = data;
    }
​
    function compute() public view returns(uint){
        return _data * 10;
    }
}

此合约包含两个能力,一个是存储数据(setData函数),另一个是运用数据进行计算(compute函数)。如果合约部署一段时间后,发现compute写错了,比如不应是乘以10,而要乘以20,就会引出前文如何升级合约的问题。

这时,可以部署一个新合约,并尝试将已有数据迁移到新的合约上,但这是一个很重的操作,一方面要编写迁移工具的代码,另一方面原先的数据完全作废,空占着宝贵的节点存储资源。

所以,预先在编程时进行模块化十分必要。如果我们将"数据"看成不变的事物,将"逻辑"看成可能改变的事物,就可以完美避开上述问题。Data Segregation(意为数据分离)模式很好地实现了这一想法。

该模式要求一个业务合约和一个数据合约:数据合约只管数据存取,这部分是稳定的;而业务合约则通过数据合约来完成逻辑操作。

结合前面的例子,我们将数据读写操作专门转移到一个合约DataRepository中:

contract DataRepository{
​
    uint private _data;
​
    function setData(uint data) public {
        _data = data;
    }
​
    function getData() public view returns(uint){
        return _data;
    }
}

计算功能被单独放入一个业务合约中:

contract Computer{
    DataRepository private _dataRepository;
    constructor(address addr){
        _dataRepository =DataRepository(addr);
    }
​
    //业务代码
    function compute() public view returns(uint){
        return _dataRepository.getData() * 10;
    }    
}

这样,只要数据合约是稳定的,业务合约的升级就很轻量化了。比如,当我要把Computer换成ComputerV2时,原先的数据依然可以被复用。

Satellite - 分解合约功能

一个复杂的合约通常由许多功能构成,如果这些功能全部耦合在一个合约中,当某一个功能需要更新时,就不得不去部署整个合约,正常的功能都会受到波及。

Satellite模式运用单一职责原则解决上述问题,提倡将合约子功能放到子合约里,每个子合约(也称为卫星合约)只对应一个功能。当某个子功能需要修改,只要创建新的子合约,并将其地址更新到主合约里即可,其余功能不受影响。

举个简单的例子,下面这个合约的setVariable功能是将输入数据进行计算(compute函数),并将计算结果存入合约状态_variable:

contract Base {
    uint public _variable;
​
    function setVariable(uint data) public {
        _variable = compute(data);
    }
​
    //计算
    function compute(uint a) internal returns(uint){
        return a * 10;        
    }
}

如果部署后,发现compute函数写错,希望乘以的系数是20,就要重新部署整个合约。但如果一开始按照Satellite模式操作,则只需部署相应的子合约。

首先,我们先将compute函数剥离到一个单独的卫星合约中去:

contract Satellite {
    function compute(uint a) public returns(uint){
        return a * 10;        
    }
}

然后,主合约依赖该子合约完成setVariable:

contract Base {
    uint public _variable;
​
    function setVariable(uint data) public {
        _variable = _satellite.compute(data);
    }
​
     Satellite _satellite;
    //更新子合约(卫星合约)
    function updateSatellite(address addr) public {
        _satellite = Satellite(addr);
    }
}

这样,当我们需要修改compute函数时,只需部署这样一个新合约,并将它的地址传入到Base.updateSatellite即可:

contract Satellite2{
    function compute(uint a) public returns(uint){
        return a * 20;        
    }    
}

Contract Registry - 跟踪最新合约

在Satellite模式中,如果一个主合约依赖子合约,在子合约升级时,主合约需要更新对子合约的地址引用,这通过updateXXX来完成,例如前文的updateSatellite函数。

这类接口属于维护性接口,与实际业务无关,过多暴露此类接口会影响主合约美观,让调用者的体验大打折扣。Contract Registry设计模式优雅地解决了这个问题。

在该设计模式下,会有一个专门的合约Registry跟踪子合约的每次升级情况,主合约可通过查询此Registyr合约取得最新的子合约地址。卫星合约重新部署后,新地址通过Registry.update函数来更新。

contract Registry{
​
    address _current;
    address[] _previous;
​
    //子合约升级了,就通过update函数更新地址
    function update(address newAddress) public{
        if(newAddress != _current){
            _previous.push(_current);
            _current = newAddress;
        }
    } 
​
    function getCurrent() public view returns(address){
        return _current;
    }
}

主合约依赖于Registry获取最新的卫星合约地址。

contract Base {
    uint public _variable;
​
    function setVariable(uint data) public {
        Satellite satellite = Satellite(_registry.getCurrent());
        _variable = satellite.compute(data);
    }
​
    Registry private _registry = //...;
}

Contract Relay - 代理调用最新合约

该设计模式所解决问题与Contract Registry一样,即主合约无需暴露维护性接口就可调用最新子合约。该模式下,存在一个代理合约,和子合约享有相同接口,负责将主合约的调用请求传递给真正的子合约。卫星合约重新部署后,新地址通过SatelliteProxy.update函数来更新。

contract SatelliteProxy{
    address _current;
    function compute(uint a) public returns(uint){
        Satellite satellite = Satellite(_current);   
        return satellite.compute(a);
    } 
    
    //子合约升级了,就通过update函数更新地址
    function update(address newAddress) public{
        if(newAddress != _current){
            _current = newAddress;
        }
    }   
}
​
​
contract Satellite {
    function compute(uint a) public returns(uint){
        return a * 10;        
    }
}

主合约依赖于SatelliteProxy:

contract Base {
    uint public _variable;
​
    function setVariable(uint data) public {
        _variable = _proxy.compute(data);
    }
    SatelliteProxy private _proxy = //...;
}

生命周期(Lifecycle)


在默认情况下,一个合约的生命周期近乎无限——除非赖以生存的区块链被消灭。但很多时候,用户希望缩短合约的生命周期。这一节将介绍两个简单模式提前终结合约生命。

Mortal - 允许合约自毁

字节码中有一个selfdestruct指令,用于销毁合约。所以只需要暴露出自毁接口即可:

contract Mortal{
​
    //自毁
    function destroy() public{
        selfdestruct(msg.sender);
    } 
}

Automatic Deprecation - 允许合约自动停止服务

如果你希望一个合约在指定期限后停止服务,而不需要人工介入,可以使用Automatic Deprecation模式。

contract AutoDeprecated{
​
    uint private _deadline;
​
    function setDeadline(uint time) public {
        _deadline = time;
    }
​
    modifier notExpired(){
        require(now <= _deadline);
        _;
    }
​
    function service() public notExpired{ 
        //some code    
    } 
}

当用户调用service,notExpired修饰符会先进行日期检测,这样,一旦过了特定时间,调用就会因过期而被拦截在notExpired层。

权限(Authorization)


前文中有许多管理性接口,这些接口如果任何人都可调用,会造成严重后果,例如上文中的自毁函数,假设任何人都能访问,其严重性不言而喻。所以,一套保证只有特定账户能够访问的权限控制设计模式显得尤为重要。

Ownership

对于权限的管控,可以采用Ownership模式。该模式保证了只有合约的拥有者才能调用某些函数。首先需要有一个Owned合约:

contract Owned{
​
    address public _owner;
​
    constructor() {
        _owner = msg.sender;
    }    
​
    modifier onlyOwner(){
        require(_owner == msg.sender);
        _;
    }
}

如果一个业务合约,希望某个函数只由拥有者调用,该怎么办呢?如下:

contract Biz is Owned{
    function manage() public onlyOwner{
    }
}

这样,当调用manage函数时,onlyOwner修饰符就会先运行并检测调用者是否与合约拥有者一致,从而将无授权的调用拦截在外。

行为控制(Action And Control)


这类模式一般针对具体场景使用,这节将主要介绍基于隐私的编码模式和与链外数据交互的设计模式。

Commit - Reveal - 延迟秘密泄露

链上数据都是公开透明的,一旦某些隐私数据上链,任何人都可看到,并且再也无法撤回。

Commit And Reveal模式允许用户将要保护的数据转换为不可识别数据,比如一串哈希值,直到某个时刻再揭示哈希值的含义,展露真正的原值。

以投票场景举例,假设需要在所有参与者都完成投票后再揭示投票内容,以防这期间参与者受票数影响。我们可以看看,在这个场景下所用到的具体代码:

contract CommitReveal {
​
    struct Commit {
        string choice; 
        string secret; 
        uint status;
    }
​
    mapping(address => mapping(bytes32 => Commit)) public userCommits;
    event LogCommit(bytes32, address);
    event LogReveal(bytes32, address, string, string);
​
    function commit(bytes32 commit) public {
        Commit storage userCommit = userCommits[msg.sender][commit];
        require(userCommit.status == 0);
        userCommit.status = 1; // comitted
        emit LogCommit(commit, msg.sender);
    }
​
    function reveal(string choice, string secret, bytes32 commit) public {
        Commit storage userCommit = userCommits[msg.sender][commit];
        require(userCommit.status == 1);
        require(commit == keccak256(choice, secret));
        userCommit.choice = choice;
        userCommit.secret = secret;
        userCommit.status = 2;
        emit LogReveal(commit, msg.sender, choice, secret);
    }
}

Oracle - 读取链外数据

目前,链上的智能合约生态相对封闭,无法获取链外数据,影响了智能合约的应用范围。

链外数据可极大扩展智能合约的使用范围,比如在保险业中,如果智能合约可读取到现实发生的意外事件,就可自动执行理赔。

获取外部数据会通过名为Oracle的链外数据层来执行。当业务方的合约尝试获取外部数据时,会先将查询请求存入到某个Oracle专用合约内;Oracle会监听该合约,读取到这个查询请求后,执行查询,并调用业务合约响应接口使合约获取结果。

图片4.png

下面定义了一个Oracle合约:

contract Oracle {
    address oracleSource = 0x123; // known source
​
    struct Request {
        bytes data;
        function(bytes memory) external callback;
}
​
    Request[] requests;
    event NewRequest(uint);
    modifier onlyByOracle() {
        require(msg.sender == oracleSource); _;
    }
​
    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }
​
    //回调函数,由Oracle调用
    function reply(uint requestID, bytes response) public onlyByOracle() {
        requests[requestID].callback(response);
    }
}

业务方合约与Oracle合约进行交互:

contract BizContract {
    Oracle _oracle;
​
    constructor(address oracle){
        _oracle = Oracle(oracle);
    }
​
    modifier onlyByOracle() {
        require(msg.sender == address(_oracle)); 
        _;
    }
​
    function updateExchangeRate() {
        _oracle.query("USD", this.oracleResponse);
    }
​
    //回调函数,用于读取响应
    function oracleResponse(bytes response) onlyByOracle {
    // use the data
    }
}

总结


本文的介绍涵盖了安全性、可维护性等多种设计模式,其中,有些偏原则性,如Security和Maintaince设计模式;有些是偏实践,例如Authrization,Action And Control。

这些设计模式,尤其实践类,并不能涵盖所有场景。随着对实际业务的深入探索,会遇到越来越多的特定场景与问题,开发者可对这些模式提炼、升华,以沉淀出针对某类问题的设计模式。

上述设计模式是程序员的有力武器,掌握它们可应对许多已知场景,但更应掌握提炼设计模式的方法,这样才能从容应对未知领域,这个过程离不开对业务的深入探索,对软件工程原则的深入理解。

下期预告

图片5.png

FISCO BCOS的代码完全开源且免费
下载地址:https://github.com/FISCO-BCOS...

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 7月22日

BSN首批“官方指定区块链应用”出炉,FISCO BCOS社区四个应用入选

历经一个多月的征集,BSN于近日公布了首批“官方指定区块链应用”名单,其中4个应用基于FISCO BCOS区块链底层研发,覆盖存证、防伪溯源、供应链管理等领域。

本着“把最合适的区块链应用,展示给最需要的用户”的目的,BSN发展联盟开发者委员会根据应用准入机制对提交作品进行了审核及综合考量,本次优先选出9种分类的12个区块链应用,作为首批入选BSN的指定应用。

基于FISCO BCOS区块链底层研发的4个应用分别是:链动时代区块链存证服务系统、农业产业全过程溯源云平台区块链应用、惠运链、伊OS透明建造平台。
0.jpeg

链动时代:区块链存证服务系统

链动时代区块链存证服务系统(以下简称“inBC存证系统”)基于BSN上的FISCO BCOS联盟链构建。inBC存证系统帮助用户基于API接口扩展已有业务系统,实现电子证据的存证保全、调用核验。可广泛应用到电子合同、版权保护、证件证书、防伪溯源、公益捐赠等场景和领域。
0-4.png

天演维真:农业产业全过程溯源云平台区块链应用

该平台充分结合物联网、区块链、云计算、大数据和地理信息等技术,在图形界面的软件环境下,实现信息采集、审核处理、控制执行、科学决策的“集成化、可视化、网络化和桌面化”。

平台通过连接生产、加工、仓储、物流和消费各个环节,梳理统一的产品标准和管控流程,规范企业生产经营行为,提升企业质量管控能力,切实保障产品品质。同时,将这些信息同步开放给消费者,增强消费者认知,打造消费信任。

0-5.png

目前,苏州市阳澄湖大闸蟹行业协会集体商标防伪追溯体系、赣南脐橙质量安全追溯示范项目等应用均使用该平台。

安链数据:惠运链

惠运链是安链科技在为物流无车承运平台、保险、银行等企业提供的物流保险供应链金融解决方案。

在货运交易和运输物流管理业务场景下,提炼多方协同的单据和信息,通过区块链技术的应用,将物流公司、保险机构、金融机构等生态链节点接入联盟链中,优化资源利用率,提高物流行业整体协作效率,利用可信数据推动保险和金融机构与物流行业融合。

0.png

目前惠运链合作用户包括南京融贸通智慧物流科技有限公司、江苏新宁现代物流股份有限公司、太平洋保险、招商银行等。

建信筑和:伊OS透明建造平台

伊OS透明建造平台是深圳市建信筑和科技有限公司研发的一款基于区块链技术应用的建造行业全生命周期管理系统。

该平台着力为工程项目构建完整的信用生态系统,运用区块链、大数据等前沿科技技术,协助甲方执行项目全生命周期的管理,让项目责任可追溯,项目管理透明化,让过程变得公平公正。目前平台已为中国雄安集团、深圳建科院未来大厦项目提供服务。

0-3.png

什么是“BSN官方指定应用”?

BSN发展联盟开发者委员会根据客户需求和业界产品的分布,划分了14个相对通用的区块链应用分类以及“其它”分类,共计15个应用分类。分类覆盖供应链管理、供应链金融、司法存证、电子合同、防伪溯源等方面。

每个区块链应用分类下只引入3个有代表性的产品解决方案。方案经开发者委员会审核通过后,将作为BSN官方指定推荐的区块链应用,在BSN的各个渠道进行广泛推荐。

第二批指定应用也在紧锣密鼓的上线准备中,如您也想加入BSN官方指定应用,欢迎与社区小助手联系。


FISCO BCOS的代码完全开源且免费

下载地址:https://github.com/FISCO-BCOS/FISCO-BCOS

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 7月17日

FISCO BCOS流量控制实现

文章来源:FISCO BCOS开源社区公众号
作者:陈宇杰/FISCO BCOS核心开发者

​1.引言

区块链系统作为分布式系统,面对大数据量突发请求场景,暴涨的请求容易引起区块链服务或接口不可用,严重时可能导致整个区块链系统陷入雪崩状态。

为了提供更稳定可靠、柔性可用的服务,FISCO BCOS v2.5版本引入了流量控制功能,从节点和群组两个维度进行限流,一方面,面对大数据量突发请求时对区块链系统进行保护,保证系统能正常运行,提升系统可用性;另一方面降低区块链节点间、群组间的资源干扰,提升区块链系统的服务质量。

2.为什么引入流量控制

FISCO BCOS引入流量控制,旨在:

  • 应对大数据量突发请求
  • 降低区块链节点间、群组间的资源干扰
  • 降低模块间的相互影响

应对大数据量突发请求

1.png

上图对比了无流量控制功能带有流量控制功能的区块链系统面对大数据量突发请求时的处理情况。

假设该区块链系统处理能力为2W,当业务以20W请求速率访问区块链节点时:

  • 无流量控制的场景下,系统对业务请求照单全收,导致内部积压的请求数目越来越多,区块链节点响应速度越来越慢,若业务持续以高于系统处理能力的速率发起请求,最终整个系统可能会陷入雪崩状态,无法响应任何业务请求。
  • 加入了流量控制功能后,流量控制模块会根据系统处理能力过滤业务请求。在业务请求速率超出系统处理能力时,流量控制模块会拒绝剩余的处理请求,使系统维持"收支平衡"的健康状态;并将请求过载的信息返回给业务,业务可根据该信息自适应地调整请求速率,对区块链系统进行保护。

简而言之,引入流量控制模块就是给区块链系统加上一层安全保护罩,让系统在接收大数据量突发请求的场景下可以健壮工作,正常响应业务请求。

降低区块链节点间/群组间资源干扰

2.png

注:图中两个节点属于两条不同的链,接入了两个不同服务

如上图,当多个区块链节点部署于同一台机器时,会出现资源竞争的问题,某些节点占用过多系统资源会影响到其他节点的正常服务。

  • t1时刻,业务1持续以1W的请求速率请求左边节点,该节点流量激增,系统接收并处理请求后,使用了90%的CPU
  • 经过t时间间隔,业务2以5000的请求速率请求右边节点,该节点资源匮乏,只能抢占到10%的CPU,响应速度很慢

上述场景中,左边节点因占用过多系统资源影响了右边节点的服务质量。引入流量控制后,可限制每个节点接收请求的速率,控制每个区块链节点的资源占用,避免因区块链节点资源竞争导致的服务质量下降或服务不可用问题。

仍以上图为例:

  • t1时刻,业务1持续以1W的请求速率请求节点1,节点1流量控制模块根据配置的请求阈值拒绝多余的请求(这里设阈值为5000),机器CPU占用率维持在50%
  • 业务1收到"流量过载"的响应后,可将其请求速率调整到5000
  • 经过t时间间隔,业务2以5000的请求速率请求节点2,此时机器还剩余50%的 CPU,足以处理5000个请求,业务2的请求得到正常响应

类似于一台机器上运行多个区块链节点时会发生资源竞争,多群组架构下,群组间也存在资源竞争,某个群组占用过多资源同样会影响到其他群组的服务质量,采用群组级别的流量控制是解决群组间资源竞争的良方。

降低模块间相互影响

同一个节点或群组内的不同模块,也存在资源竞争问题,主要是网络资源竞争,存在网络资源竞争的模块包括:

  • 共识模块
  • 交易同步模块
  • 区块同步模块
  • AMOP模块

其中共识模块、交易同步模块是决定区块链系统服务质量的关键模块,其他模块过多占用网络资源,会影响这些关键模块,进而影响系统可用性。

FISCO BCOS实现了模块级别的流量控制,通过控制非关键的网络流量,优先保证关键模块服务质量,提升系统健壮性。

3. 流量控制的功能

3.png

FISCO BCOS从节点和群组两个维度实现了业务到节点的请求速率限制和模块粒度的网络流量限制。前者限制业务到节点的请求速率,以应对大数据量突发请求,保证区块链节点的柔性服务;后者通过限制区块同步、AMOP等非关键模块的网络流量,优先保证共识、交易同步等关键模块的性能和稳定性。

  • 节点级别请求速率限制:限制业务到节点的总请求速率,当请求速率超过指定阈值后,节点会拒绝业务请求,避免节点过载,防止过多的请求导致节点异常;控制节点资源使用量,降低区块链节点之间的资源竞争
  • 节点级别的流量控制:限制节点的平均出带宽,当节点平均出带宽超过设置阈值后,节点收到区块同步请求后会暂缓发送区块、拒绝收到的AMOP请求,避免区块同步、AMOP消息包发送对节点共识的影响

群组维度上,主要功能包括:

  • 群组级别请求速率限制:限制业务到群组的请求速率,当请求速率超过阈值后,群组会拒绝业务请求,该功能可在大数据量突发请求的场景下保护区块链节点,控制群组资源使用量,降低群组间的资源竞争
  • 群组级别的流量控制:限制每个群组的平均出带宽,当群组平均出带宽流量超过设置阈值后,该群组会暂停区块发送和AMOP请求包转发逻辑,优先将网络流量提供给共识模块使用

当节点和群组都开启请求速率限制时:

节点收到业务发送的请求包时,首先调用节点级别请求速率限制模块判断是否接收该请求,如请求被接收,则进入群组级别请求速率限制模块,通过该模块检查后的请求才会被转发到相应群组,进行处理。

当节点和群组都开启网络流量控制功能时:

1、节点收到客户端AMOP请求,首先调用节点级流量控制模块判断是否接收该AMOP请求

2、当某个群组收到其他节点对应群组的区块请求后,群组在回复区块之前,需要:

  • 调用节点级流量控制模块,判断节点平均出带宽是否超过设置阈值
  • 调用群组级流量控制模块,判断群组出带宽是否超过设置阈值,当且仅当节点级和群组级平均出带宽均未超过设置阈值时,该群组才会回复区块请求

4.如何使用流量控制功能

流量控制配置分别位于config.ini和group.i.ini配置文件的[flow_control]配置项中,分别对应为节点级别流量控制配置和群组级别流量控制。这里向大家介绍如何启用、关闭、配置流量控制。

节点级流量控制

节点级别的网络流量控制配置项均位于config.ini配置文件中,主要包括:

请求速率限制

节点级别的请求速率限制位于配置项[flow_control].limit_req中,用于限制业务每秒到节点的最大请求数目,当请求数目超过设置阈值时,请求会被拒绝。该配置项默认关闭,若要开启,请将limit_req配置项前面的;去掉。

打开请求速率限制并设计节点每秒可接受2000个业务请求的示例如下:

[flow_control]
  ; restrict QPS of the node
  limit_req=2000
网络流量限制
  • [flow_control].outgoing_bandwidth_limit:节点出带宽限制,单位为Mbit/s,当节点出带宽超过该值时,会暂缓区块发送,也会拒绝客户端发送的AMOP请求,但不会限制区块共识和交易广播的流量。该配置项默认关闭,若要开启,请将outgoing_bandwidth_limit配置项前面的;去掉。

打开节点出带宽流量限制,并将其设置为5MBit/s的配置示例如下:

[flow_control]
  ; Mb, can be a decimal
  ; when the outgoing bandwidth exceeds the limit, the block synchronization operation will not proceed
  outgoing_bandwidth_limit=5

群组级流量控制

群组级别的网络流量控制配置项均位于group.i.ini配置文件中,主要包括:

请求速率限制

群组i的请求速率限制位于group.i.ini的配置项[flow_control].limit_req中,限制业务每秒到群组的最大请求数目,当请求数目超过配置项的值时,请求会被拒绝。该配置项默认关闭,若要开启,请将limit_req配置项前面的;去掉。

打开请求速率限制并配置群组每秒可接受1000个业务请求的示例如下:

[flow_control]
  ; restrict QPS of the group
  limit_req=1000
群组内网络流量限制

[flow_control].outgoing_bandwidth_limit:群组出带宽限制,单位为Mbit/s,当群组出带宽超过该值时,会暂缓发送区块,但不会限制区块共识和交易广播的流量。该配置项默认关闭,若要开启,请将outgoing_bandwidth_limit配置项前面的;去掉。

打开群组出带宽流量限制,并将其设置为2MBit/s的配置示例如下:

[flow_control]
  ; Mb, can be a decimal
  ; when the outgoing bandwidth exceeds the limit, the block synchronization operation will not proceed
  outgoing_bandwidth_limit=2

5. 总结

随着区块链技术的发展,越来越多应用部署于区块链系统中,对区块链系统服务质量的要求也日渐提升,区块链系统的柔性可用、稳定健壮变得更加重要。

FISCO BCOS v2.5引入流量控制功能,是FISCO BCOS对区块链柔性服务探索的重要一步。

社区将持续打磨,优化区块链系统服务质量,希望未来能为海量业务场景提供更好的、高可用的柔性服务。

如何做好流量控制的同时,又不影响原本系统性能?敬请关注社区后续文章,为您详解流量控制策略的具体实现原理。

欢迎大家共同探讨交流,积极反馈使用的体验与改进建议。

FISCO BCOS的代码完全开源且免费

**下载地址:https://github.com/FISCO-BCOS...

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 7月11日

FISCO BCOS 角色权限模型的实现

图片1.jpg

引言

FISCO BCOS的权限控制是通过控制账号对系统中表的写权限来实现的。这种权限控制模型非常灵活且强大,用户几乎可以控制任意一项权限,例如,通过控制权限表的写权限管理分配权限;通过控制系统合约所对应表的写权限管理链配置、节点身份管理、合约部署、用户表创建等;通过控制合约表的写权限管理合约写接口的调用。

然而,绝对完美是不存在的。强大灵活的权限控制也带来较高的学习成本:用户需要理解每个权限项控制的内容以及如何设置,了解链管理员和系统管理员的区别……大量的概念和操作,对用户要求极高。

为了降低使用难度,提升用户体验,FISCO BCOS v2.5对此功能进行了优化,新增了基于角色的权限控制。把不同的权限统归到不同角色,用户根据账号所属角色即可判断其所拥有的权限。同时v2.5基于角色引入了链上治理投票模型,使治理操作更加方便。

什么是角色权限模型?

使用角色权限模型后,用户只需记住角色,而角色对应的权限不言自明,例如,治理委员会委员拥有链治理相关权限,这极大降低用户理解难度与学习成本。

图片2.jpg

角色对应的权限

区块链上的参与者,可根据角色分为治理方、运维方、监管方和业务方。为避免既当裁判又当运动员,治理方、运维方应权责分离,角色互斥。

  • 治理方:角色称为治理委员会委员,简称委员,负责区块链治理。
  • 运维方:负责区块链运维,该角色由委员添加。
  • 业务方:业务方账号由运维添加到某个合约,可以调用该合约的写接口。
  • 监管方:监管方监管链的运行,能够获取链运行中权限变更记录以及需要审计的数据。

各角色所对应权限具体如下表所示。

图片3.jpg

角色权限实现的细节

本小节将简单介绍委员、运维和业务角色的权限实现细节以及背后原理,以便更好理解与使用角色权限功能。

链初始无委员账号,当存在至少一个委员账号时,委员拥有的权限开始受到控制。联盟链实际应用中多个参与方的技术实力可能并不相同,从实际应用场景出发,我们引入了链上治理投票模型,所有治理操作需要有效投票数/委员数>生效阈值才能生效,用户通过新增的链治理预编译合约可以实现委员的增删、权重修改、投票生效阈值修改等操作。

投票模型有几处值得注意:

  • 每次投票操作,如果是委员投票,则记录操作内容和投票委员,不重复计票
  • 每次投票操作,计票结束后,计算有效投票数/委员数,如果大于此操作的生效阈值,则对应操作生效
  • 投票设置过期时间,根据块高,blockLimit的10倍,固定不可改

运维角色的新增与撤销必须由委员角色操作。链初始无运维账号,当存在至少一个运维账号时,运维拥有的权限开始受到控制。

业务账号可以调用链上查询接口与运维指定合约的写接口。

兼容性说明

目前,角色权限模型基于对系统中各类表的写权限控制。我们做了最大努力与之前版本使用体验保持一致,但为了权限控制的完整严谨,对FISCO BCOS v2.5新建链,控制台grantPermissionManager指令不再有效,原PermissionManager的权限归属于委员角色。对于v2.5之前的链,该指令仍然有效。

如何使用角色权限?

本节将以“委员增删”和“运维增删”为例进行简要的实操演示,文档含括了更丰富的角色权限相关操作,欢迎移步查看。

https://fisco-bcos-documentat..._CN/latest/docs/manual/permission_control.html

增删委员

使用控制台v1.0.10以上版本中自带的get_account.sh脚本,生成3个如下账号,接下来的操作以这3个账号为例演示。配置好控制台后,使用控制台的-pem选项分别加载3个私钥启动3个控制台。

# 账号1
0x61d88abf7ce4a7f8479cff9cc1422bef2dac9b9a.pem
# 账号2
0x85961172229aec21694d742a5bd577bedffcfec3.pem
# 账号3
0x0b6f526d797425540ea70becd7adac7d50f4a7c0.pem

添加账号1为委员

增加委员需要链治理委员会投票,有效票大于阈值才可生效。此处由于只有账号1是委员,所以账号1投票即可生效。

图片4.jpg

使用账号1添加账号2为委员

此处由于只有账号1是委员,所以使用账号1投票后,满足阈值判断立刻生效。

图片5.jpg

撤销账号2的委员权限

此时系统中有账号1和账号2两个委员,默认投票生效阈值50%,所以需要两个委员都投票撤销账号2的委员权限,有效票/总票数=2/2=1>0.5才满足条件。

账号1投票撤销账号2的委员权限,如下图:

图片6.jpg

账号2操作投票撤销账号2的委员权限,如下图:

图片7.jpg

增删运维

委员可以添加与撤销运维角色,运维角色的权限包括部署合约、创建表、冻结解冻所部署的合约、使用CNS服务等。

使用账号1添加账号3为运维

图片8.jpg

使用账号3部署HelloWorld

账号3是运维角色,可以部署合约,具体操作如下图:

图片9.jpg

使用账号1部署HelloWorld

账号1是委员,不具有部署合约的权限,部署合约失败,如下图:

图片10.jpg

使用账号1撤销账号3的运维权限

账号1是委员可以撤销运维,如下图:

图片11.jpg

总结

作为联盟链的重要特性,权限控制需要做到灵活且强大,但如何在此基础上达成良好的用户体验,需要不断地改进优化。

FISCO BCOS v2.5前的权限控制实现了灵活且强大的功能,但同时,社区也收到不少反馈,认为权限控制的使用理解门槛过高。通过角色权限,我们希望能在保持原有功能的同时,降低学习门槛、提升大家的使用体验。整合优化权限控制的工作仍在进行,希望未来能实现从底层到应用全覆盖的权限控制解决方案。

欢迎大家共同探讨交流,积极反馈使用体验与改进建议。

图片12.jpg

FISCO BCOS的代码完全开源且免费下载地址↓↓↓

https://github.com/FISCO-BCOS...

图片13.jpg

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 7月4日

如何解释“我篡改了区块链”这个问题

图片1.jpg

区块链数据“全局一致”、“难以篡改”这两个特性已经广为人知,是区块链营造“信任”的基石。为了达到这两个效果,区块链的共识、同步、校验等技术细节足可大书特书,而本文要从“我篡改了区块链数据”讲起。

“我篡改了区块链数据”

FISCO BCOS开源联盟链社区现在相当活跃,每天都会产生大量的讨论,大家也会饶有兴趣地研究和挑战区块链如何做到“难以篡改”。我们注意到,尤其在FISCO BCOS支持MySQL数据库作为数据存储引擎后,隔一阵子就有同学在群里问:“我手动修改了我节点连接的数据库里某个状态数据,这是不是就是篡改了区块链数据呢?”

直观地举个例,如链上有个智能合约,管理特定资产余额,在数据库合约表里,经过共识的Alice的余额本来是100,这时有人打开MySQL客户端,找到那个合约对应的Table,把Alice的余额update成10000

这时他表示:“你看,我调用合约的查询接口,查出来Alice的余额确实是10000,这就不对了嘛,而且,链还在出块,根本不防篡改嘛!”。

初步分析和解答

为何这类问题最近多起来了?我们分析了下,猜想主要是由于MySQL数据库用户基础良好,体系比较成熟,给用户提供友好的命令行或图形化交互工具,FISCO BCOS提供了一种Table风格的合约开发模式,表结构设计得清晰直观,对于用户来说,一方面理解和管理起来更容易了,另一方面顺手update甚至delete一下都是小意思。

下图仅为示例数据,采用KVTable合约方式,创建了名为t_kv_node的合约表,系统自动加了“u_”前缀,可见,这个表结构和数据一目了然。

图片2.jpg

而之前只采用LevelDB或RocksDB作为存储引擎,这两个文件型数据库交互工具比较少,在用户面前的存在感不强,操作相对晦涩,主要通过API编程访问,数据用肉眼难以辨别的Hash key寻址,动手修改数据库的case就少了一些(但也不是不可能)。

所以,热点问题浮出水面,前提是用户可以更方便地修改底层数据了,而不是这个问题之前不存在。

这时我们会建议用户试一下,针对Alice的余额,发起一个交易,比如给Alice充值,或者让Alice转账操作,这时,修改过数据的节点将无法参与共识。因为该节点上算出来的Alice的余额和其他节点结果不同,其他节点依旧按100的余额进行计算,而不是10000,显然结果是对不齐的。

图片3.png

复习下PBFT的容错模型:定义“f”为可容错节点数,网络中共识节点总数应等于或多于3f+1。即链上有4个共识节点时,可容错的f=1,共识节点总数为7个时,f=2,以此类推。

如果未修改过数据的节点数满足PBFT要求的2f+1的数量,链依旧可以出块。但被修改过的节点,一旦有交易涉及脏数据,就像踩到了雷一样,从此再无法与链共识、同步,相当于被抛弃了。这种节点可以称为“拜占庭节点”,即作恶或出错的节点,具备节点准入控制能力的联盟链甚至会将拜占庭节点隔离出去。

还有一种可能性是,手动修改了数据库里的数据,但节点内存里还刚好缓存了一份副本,并没有被修改,所以通过节点对这个数据的查询、交易还是正常的,甚至会用正确的结果把数据库里被篡改过的数据覆盖掉,不过这是概率性事件,取决于缓存的大小和当时包含的数据项。

*注:对于采用PoW或其他共识机制的链来说,容错模型有所不同,但在容错范围内的少数节点被篡改,也不会影响链的共识。

“能否篡改整个联盟链”

有的同学可能会继续刨根问底:“那我多修改几个节点的数据是不是就篡改了?”,一般提出这个问题的同学是面向他自己部署的开发测试环境,所有节点都在他手上,所以可以随便改。

在真实的联盟链环境上,节点分别掌握在不同机构手里,要修改,首先得侵入他人的网络、获得服务器和数据库权限、发起修改再全身而退。事实上,在注重安全防护的商业化环境里,这是非常艰难、几乎不可能做到的事情。

图片4.jpg

从机构粒度来看,单个机构掌握的节点数,应该低于共识算法可容错的数量。比如,链上总共有7个共识节点,那么单个机构掌握的共识节点不应多于2个,这样可以避免机构内部强行修改自己掌握的节点数据,或一个机构的所有节点都意外出错、掉线(比如机房光纤都被挖断了),导致链无法出块。

图片5.jpg

真的没有办法防“本地篡改”吗?

考虑区块链数据本地验证的机制,比如区块之间的Hash关系、状态的Merkle树结构、共识节点的签名等,按数据的互验关系顺藤摸瓜进行检测,似乎有一定概率可以本地检测出数据异常。

但进一步想,对某个数据的查询,区块链的本地校验范围是有限的,一般不会超出单个区块或者一棵merkle树,所以如果篡改者比较熟悉区块链数据的结构和本地校验逻辑,也可以顺着数据校验关系,从状态值开始,把merkle树、区块Hash等关键数据全部改掉。

甚至更彻底地,从创世块开始,把所有的区块、系统配置(对于PoW,可以修改挖矿难度以加速出块)、PBFT的共识者列表等等,都按他的逻辑改一遍,这样这条本地数据链依旧是校验自洽的,只是无法和其他节点共识了。

这种改法,听起来需要不少力气活,但对于一个有决心、有能力的篡改者来说,改改本地数据这个事情其实并不难,难的只是去改别的机构数据而已。

图片6.jpg

到了这个份上,就相当于一个人铁了心要“骗自己”,那神仙都没有办法了。一旦把本地数据修改的权限交给了不适当的人,最坏情况下,整条链没有一个字节是对的。

但是,本地数据再错,也只会影响自己,影响不了别人,一旦和其他节点进行共识,就会被发现,甚至被惩戒,整个效果会有一点掩耳盗铃的意思。

“为什么区块链不拦住我篡改数据?”

再进一步,那位同学又会问:“为什么区块链不能立刻发现、并且阻止我篡改数据?也许我只是无意手误呢”。坦率说,这有点对区块链期望过高了。

区块链系统并非无所不能,也不会包办一切,区块链并不会阻止用户对自己的服务器、软件、数据库等施加操作,就像法律不能也不应去阻止你打碎家里的杯子一样。

本质上,区块链的一致性、难以篡改性是面向“全局”的,是由多方博弈和协作达成的,当链上交易牵涉错误数据时,共识机制可以检测并拒绝已被篡改的数据,保证链上剩余的大多数健康节点继续共识出块。而节点本地不参与共识的数据,共识机制鞭长莫及。

那么,区块链为什么不能主动检测和纠正错误,保证每个节点上的数据一致性?首先,链上的数据非常庞杂,用户直接登入数据库手动修改少量数据,区块链节点并不知道哪一条数据被修改了,无法触发检查。

如果区块链系统定期巡检所有数据,并将所有数据和其他节点进行比对,可想而知,这样做的话,网络、磁盘和计算开销会非常大。

关键是,这并不解决问题,因为从数据被篡改后到检测出来的时间窗里,哪怕脏数据只存在了几十毫秒,但这时如果不幸有应用来查询数据,依旧会得到篡改后的结果。对要求苛刻的业务来说,事后检测未必是最佳选项,因为有可能已经造成了业务损失,届时能做的最多就是告警和冲正了。

当然,也可以结合数据库的操作监控、binlog等辅助机制,加速响应速度和检测效率。方法还是有的,如上所述,只是性价比较低,也不彻底解决问题,只有对数据修改极其敏感,且业务上接受延时发现和修订的特定场景,才会考虑将其作为补救措施。我们把这部分归类到运营管理工具里,根据场景需求来实现。

还有一种方法,可以部分解决查询问题:f+1查询。即查询数据时,无论是查区块数据,还是合约的状态数据,不妨多查几个节点,查询节点数多于 f 即可。

如链上有个7个节点时,f=2,用户查询自己节点之外,继续发出网络请求去查询其他机构的2个节点,共查询3个节点,如果得到的数据都是一致的,则表示数据一定是正确的,反之,一定是这3个节点里出了问题。

图片7.jpg

但是,要执行f+1查询,前提是其他机构开了查询接口权限,让你连接上去查询。在很多安全防护严密的联盟链上,一般只打开节点之间P2P互联的网络端口,不会轻易给其他机构提供数据查询权限。再则,在网络上发起多次查询,其异步性、时效性、成功率和性能表现都会带来更多变数。

综上所述,对节点本地的数据,就像打地鼠,冒头的(发出交易参与共识,或进行f+1查询),区块链全局共识和容错机制能发现,没有冒头仅蹲在用户硬盘里的,只能用户自己负责了。

结语

区块链通过网络博弈、多方校验实现了全网的容错防作恶,而区块链同步给到各节点的本地数据,需要用户自行妥善管理保存。

从信任传递来看,首先用户得“信自己”,如果连自己都无法相信,说明系统和数据管理有漏洞,莫说是修改数据了,在本地系统的整个链路上,包括区块链软件、SDK、业务服务都有可能出错和篡改作假,这样的环境有何信任可言?

节点持有者必须非常审慎,首先不要手痒或手误去改数据,然后关键是要建立周全的制度,包括管理策略和技术防护,比如,主机访问控制、数据库登录和操作权限控制、操作审计、日志审计等,以避免本机构内有人越权访问监守自盗,或者被外部渗透。

万一数据出错,区块链比中心化系统好一点的就是,还有可能通过与其他节点互相校验检测出来,这时则应该进行告警、查证、补正和追责,以及在有条件和有必要的前提下,善用f+1查询方法,给查询操作加一点点保险。

另外,建议定期备份节点数据到安全的离线设备上,这样无论是出现意外还是人为的数据问题,依旧可以从冷数据里快速恢复,保证一定的RTO(复原时间目标)和RPO(恢复点目标)。

而区块链的健壮性在于,无论单个角色怎么折腾自己的节点和数据,对全局是没有影响的,只有修改者自己受损。任凭窝里翻天覆地,链上依旧云淡风轻,其“全局一致”、“难以篡”的定律依旧成立,链仍然是信任的锚点,这就是区块链的魅力。

图片8.jpg

FISCO BCOS的代码完全开源且免费

下载地址↓↓↓

https://github.com/FISCO-BCOS...

图片9.jpg

查看原文

赞 13 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 6月9日

记一次CachedStorage中死锁的调试经历

图像1.jpg

在整合FISCO BCOS非国密单测与国密单测的工作中,我们发现CachedStorage的单测偶然会陷入卡死的状态,且可在本地持续复现。

复现方式为循环执行CachedStorage单测200次左右,便会发生一次所有线程均陷入等待状态、单测无法继续执行的情况,我们怀疑在CachedStroage中发生了死锁,故对此进行调试。

Debug思路

中医治病讲究望闻问切,调试bug同样需要遵循寻找线索、合理推断、验证解决的思路。

观察线程栈

在死锁发生时,使用/usr/bin/sample工具(mac平台环境下)将所有的线程的栈打印出来,观察各线程的工作状态。

从所有线程的线程栈中观察到有一个线程(此处称为T1)卡在CachedStorage.cpp的第698行的touchCache函数中,具体的代码实现可以参考:

https://github.com/FISCO-BCOS...

图像2.jpg

从代码片段中可以看到,T1在第691行已经获得了m_cachesMutex的读锁:代码RWMutexScoped(some_rw_mutex, false)的意思是获取某个读写锁的读锁;相应地,代码RWMutexScoped(some_rw_mutex, true)的意思是获取某个读写锁的写锁,这里的RWMutex是一个Spin Lock。

随后在第698行处尝试获取某个cache的写锁。

除了T1,还有另外一个线程(此处称为T2)卡在CachedStorage.cpp的第691行的touchCache函数中:

图像3.jpg

从代码片段中可以看到,T2在第681行已经获得了某个cache的写锁,随后在第691行处尝试获取m_cachesMutex的读锁。

继续观察后还发现若干线程卡在CachedStorage.cpp第673行的touchCache函数中:

图像4.jpg

最后还有一个Cache清理线程(此处称为T3)卡在CachedStorage.cpp的第734行的removeCache函数中:

图像5.jpg

从代码片段中可以看到,这些线程均没有持任何锁资源,只是在单纯地尝试获取m_cachesMutex的写锁。

读写饥饿问题

初期分析问题时,最诡谲的莫过于:在T1已经获取到m_cachesMutex读锁的情况下,其他同样试图获取m_cachesMutex读锁的线程竟然无法获取到。

但是看到T3线程此时正努力尝试获取m_cachesMutex写锁,联想到读写锁饥饿问题,我们认为其他线程获取不到读锁的问题根源很可能就在T3。

所谓读写锁饥饿问题是指,在多线程共用一个读写锁的环境中,如果设定只要有读线程获取读锁,后续想获取读锁的读线程都能共享此读锁,则可能导致想获取写锁的写线程永远无法获得执行机会(因为读写锁一直被其他读线程抢占)。

为了解决饥饿问题,部分读写锁会在某些情况下提高写线程的优先级,即由写线程先占用写锁,而其他读线程只能在写线程后乖乖排队直到写线程将读写锁释放出来。

在上述问题中, T1已经获取了m_cachesMutex的读锁,若此时T3恰好获得时间片并执行到CachedStorage.cpp的第734行,会因获取不到m_cachesMutex的写锁而卡住,随后其他线程也开始执行并到了获取m_cachesMutex读锁的代码行。

若读写防饥饿策略真的存在,那这些线程(包括T2)的确会在获取读锁阶段卡住,进而导致T2无法释放cache锁,从而T1无法获取到cache锁,此时所有线程均会陷入等待中。

在这个前提下,似乎一切都能解释得通。上述流程的时序图如下所示:

图像6.jpg

我们找到了TBB中Spin RW Lock的实现代码,如下图所示:

获取写锁:

图像7.jpg

获取读锁:

图像8.jpg

在获取写锁的代码中,可以看到写线程如果没有获取到写锁,会置一个WRITER_PENDING标志位,表明此时正有写线程在等待读写锁的释放,其他线程请勿打扰。

而获取的读锁代码中,也可以看到,如果读线程发现锁上被置了WRITER_PENDING标志位,就会老实地循环等待,让写线程优先去获取读写锁。这里读写锁的行为完美符合之前对读写锁防饥饿策略的推测,至此真相大白。

既然找到了问题起因,那解决起来就容易多了。在CachedStorage的设计中,Cache清理线程优先级很低,调用频率也不高(约1次/秒),因此给予它高读写锁优先级是不合理的,故将removeCache函数获取m_cachesMutex写锁方式做如下修改:

图像9.jpg

修改后,获取写锁方式跟获取读锁类似:每次获取写锁时,先try_acquire,如果没获取到就放弃本轮时间片下次再尝试,直到获取到写锁为止,此时写线程不会再去置WRITER_PENDING标志位,从而能够不影响其他读线程的正常执行。

相关代码已提交至2.5版本中,该版本将很快与大家见面,敬请期待。

实现效果

修改前循环执行CachedStorage单测200次左右便会发生死锁;修改后循环执行2000+次仍未发生死锁,且各个线程均能有条不紊地工作。

经验总结

从这次调试过程中,总结了一些经验与大家分享。

首先,分析死锁问题最有效的仍然是“两步走”方法,即通过pstack、sample、gdb等工具看线程栈,推测导致发生死锁的线程执行时序。

这里的第二步需要多发挥一点想象力。以往的死锁问题往往是两个线程间交互所导致,教科书上也多以两个线程来讲解死锁四要素,但在上述问题中,由于读写锁的特殊性质,需要三个线程按照特殊时序交互才可以引发死锁,算是较为少见的情况。

其次,『只要有线程获取到读锁,那其他想获取读锁的线程一定也能获取读锁』的思维定势是有问题的。

至少在上面的问题中,防饥饿策略的存在导致排在写线程后的读线程无法获取读锁。但本文的结论并非放之四海而皆准,要不要防饥饿、怎么防饥饿在各个多线程库的实现中有着不同的取舍。有的文章提到过某些库的实现就是遵循『读线程绝对优先』规则,那这些库就不会遇到这类问题,所以仍然需要具体问题具体分析。

《超话区块链》话题征集:我的Debug经历

欢迎与我们聊聊你经历过的有趣/难忘的debug过程,我们将挑选具备参考意义的debug经历与更多开发者分享,经社区采纳将可获得FISCO BCOS纪念衫一件。

Debug经历征集传送门

https://jinshuju.net/f/i8m0Il

图片14.jpg

FISCO BCOS的代码完全开源且免费下载地址↓↓↓

https://github.com/FISCO-BCOS...

图片15.jpg

查看原文

赞 0 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 6月2日

带你读源码:四大视角多维走读区块链源码

图片1.jpg

引子

区块链作为「新基建」的重要组成部分,越来越受技术爱好者关注。区块链极客信奉“code is law”,相信通过代码可以构筑一个可信的世界。

而作为一门综合学科技术,区块链建立在数学、密码学、计算机原理、分布式网络和博弈论等众多基础学科之上,底层代码动辄数十万行,如果没有摸清门道,要完全掌握这些代码是极具挑战的。

本文希望给读者一个走读区块链源码的方法,让读者面对区块链底层项目时可以从容地说出“show me the code”。

基础知识储备

区块链是一门综合学科,涉及多个专业领域,涵括多方面的基础知识,在深度研究区块链之前需要做一定广度的知识储备。注意,这里说的是广度,并非深度,也就是说你只需要大概知道这些基础知识的基本原理与作用即可。

图片2.jpg

  • 密码学相关:理解哈希、对称加密、非对称加密以及数字签名的基本原理和作用;
  • 计算机操作系统相关:理解多进程、多线程、互斥、并行等相关概念和作用;
  • 数据结构相关:理解队列、堆栈、树等基本数据结构和使用场景;
  • 计算机网络相关:理解TCP/IP、心跳包、消息流等基本概念;
  • 数据库相关:理解数据库基本概念,了解KV数据库的基本原理;
  • 计算机原理相关:理解程序编译、解析、执行和字节码、虚拟机等概念;
  • 分布式系统相关:理解点对点网络、分布式一致性、CAP等相关概念和基本原理;
  • 程序开发相关:掌握相关的编程语言、构建工具等,理解项目构建基本流程。

多维走读

在储备了相关的基础知识之后,你就可以打开一份真正的区块链底层代码了,一般通过git clone可以快速下载到项目代码。

但是,面对数十万行的代码,该从何看起呢?

庖丁为文惠君解牛,手之所触,肩之所倚,足之所履,膝之所踦,砉然向然,奏刀𬴃然,莫不中音:合于《桑林》之舞,乃中《经首》之会。

出自《庄子·养生主》

一个优秀的区块链底层项目,必然有一份优秀的工程代码,这份代码有其合理的组织结构与纹理逻辑。走读代码应效仿庖丁解牛,先摸清区块链的基本结构和逻辑,再开始走读,可以达到事半功倍的效果。

本文推荐要从四个不同视角进行走读,站在自己的需求角度出发去看代码,而不要被巨量的代码所左右。这四个角度为功能视角、系统视角、用户视角和开发视角,分别从逻辑层面、运行层面、使用层面和开发层面厘清代码架构和关键算法。

图片3.jpg

功能视角

在深入一份区块链底层代码之前,首先要通过其官网、技术文档、github wiki等渠道获取项目设计文档,了解其基本功能设计。

一般每个项目都会提供核心功能列表、总体架构图、功能模块图等介绍文档,通过这些介绍可以掌握项目基本功能。即使你真的找不到也不打紧,大部分区块链底层项目在功能设计层面的差异较小,核心功能模块也大致相同。

图片4.jpg

以FISCO BCOS为例,基础层代码如下:

图片5.jpg

核心层核心代码如下:

图片6.jpg

接口层核心代码如下:

图片7.jpg

从功能视角出发,先定位核心功能模块的代码位置,再仔细深入各个功能代码,从单个功能模块内,也可继续递归采用功能视角拆分法,广度遍历直至了解全貌。

系统视角

系统视角从整个区块链网络运行角度,关注区块链节点全生命周期所参与的系统行为。

关注点包括从敲下启动节点的命令开始,节点经历了哪些初始化环节,之后又是如何与其他节点建立点对点网络,以及完成分布式协作的。

由于不同区块链在部署架构上略有差异,系统运行方式也有所不同,但万变不离其宗,系统视角来看,每个区块链系统都要经历节点初始化、建立点对点网络、完成分布式交互的过程。

图片8.jpg

从系统视角看区块链,首先要关注初始化工作。以FISCO BCOS为例,区块链节点启动从main函数入口进入,通过libinitializer模块初始化并启动各模块,启动顺序如下:

图片9.jpg

通过启动顺序可以知道FISCO BCOS的一个重要特性——支持多群组账本,每个群组是一个独立的Ledger模块,每个Ledger具有独立的存储、同步、共识处理功能。

完成初始化工作同时,系统将会启动若干线程(或者进程、协程,原理类似),这些线程包括网络监听、共识、消息同步等,可以结合代码分析与系统命令查看运行节点配合确定有哪些关键线程,搞清楚关键线程的工作机制就可以基本掌握区块链系统运行机制。

以FISCO BCOS为例,节点启动之后的关键线程以及他们之间的关系如下:

图片10.jpg

初始化完成之后,网络模块的Host线程将根据配置列表,主动与其他节点建立连接,并且持续监听来自其他节点的连接;Sync线程开始相互发送区块高度,发现高度低于其他节点则开启下载逻辑;RPC与Channel线程等待客户端发送请求,将收到的交易塞入txpool;Sealer线程从txpool获取交易,Consensus线程则开始处理共识消息包。

如此,整个区块链系统有条不紊地运转,完成客户端请求与分布式协作。

用户视角

用户视角关注操作接口和交易生命周期,关注访问区块链的接口和协议设计、编解码方式、核心数据结构、错误码规范等,还会关注如何发送一笔交易到链上,交易在链上又经历了哪些处理流程,直到达成全网共识。

一般区块链底层项目都会给出交互协议的说明文档,通常实现包括JsonRPC、gRPC、Restful等不同类型的交互协议。

不同项目的交互接口会有所不同,但大都会包含发送交易、部署合约、调用合约、查看区块、查看交易以及回执、查看区块链状态等接口。不同项目的数据编码也会有所不同,有些采用Json,有些采用protobuf等。

当从技术文档中了解清楚交互协议、接口、编解码和错误码等设计细节之后,接下来最重要的是通过发送交易、部署合约、调用合约这些关键接口,对代码进行抽丝剥茧,贯穿交易整个生命周期,从而搞清楚区块链底层最核心的逻辑。

以FISCO BCOS为例,通过多个模块相互协作,完成交易整个生命周期的处理:

图片11.jpg

开发视角

开发视角关注的是整个代码工程,包括第三方依赖,源码模块之间的相互关系,单元测试框架和测试用例,编译和构建方式,持续集成和benchmark,以及如何参与社区源码贡献等等。

不同语言都有相应推荐的编译构建方式以及单测框架,通常在区块链项目源码目录可以快速定位到第三方依赖库,比如以cmake构建的C++项目有CmakeLists.txt文件,go项目有go.mod文件,rust项目有cargo.toml文件等。

图片12.jpg

以FISCO BCOS为例,从CmakeLists.txt可以看到依赖库包括:

图片13.jpg

项目核心源码包括fisco-bcos程序入口代码,以及libxxx的各模块代码,根据模块的名字可以快速识别其对应功能,这里也体现了一个项目源码质量的高低,高质量的代码应该是“代码即注释”。

单元测试代码在test目录,采用boost的单元测试框架,子目录unittests中单测代码与源码目录一一对应,非常容易找到源码对应的单元测试代码。

构建和持续集成工具代码在tools目录,子目录ci中维护了多个不同场景的持续集成用例,在github提交的每一个pr(pull request)都会触发这些持续集成用例,当且仅当每个用例成功通过方可允许合入pr。

关于FISCO BCOS的代码规范和贡献方式,在CODING_STYLE.md和CONTRIBUTING.md文件中有详细描述,鼓励社区用户积极参与贡献。

总结

区块链涉及领域和知识较多,需要深入源码细节,才能真正完全掌握区块链核心技术。所谓“重剑无锋,大巧不工”,掌握源码走读的基本方法论,才能在巨量代码前,面不改色心不跳。

本文提出从功能、系统、用户和开发四个不同视角进行区块链底层代码走读的方法,一般来说,依次选择不同视角进行走读是比较推荐的方式,也可以根据个人喜好和能力模型选择视角顺序。

最后,本文所举示例皆为FISCO BCOS,但这套走读方法可以适用于任何其他区块链底层项目,希望本文对你有所帮助。

图片14.jpg

FISCO BCOS的代码完全开源且免费下载地址↓↓↓

https://github.com/FISCO-BCOS...

图片15.jpg

查看原文

赞 8 收藏 0 评论 0

FISCO_BCOS 发布了文章 · 5月15日

区块链必读“上链”哲学:“胖链下”与“瘦链上”

什么是“上链”?什么数据和逻辑应该“上链”?文件能不能上链?链上能不能批量查数据?“链下”又是什么?

“链上”、“链下”诸多问题,一文说清。

什么是“链上”和“链下”

图片1.jpg

区块“链”的链,包含“数据链”和“节点链”。数据链指用链式结构组织区块数据,构成数据校验和追溯的链条;“节点链”指多个节点通过网络连接在一起,互相共享信息,其中的共识节点则联合执行共识算法,产生并确认区块。

交易“上链”的简要过程如下:

  1. 记账者们收录交易,按链式数据结构打包成“区块”。
  2. 共识算法驱动大家验证新区块里的交易,确保计算出一致的结果。
  3. 数据被广播到所有节点,稳妥存储下来,每个节点都会存储一个完整的数据副本。

交易一旦“上链”,则意味着得到完整执行,达成了“分布式事务性”。简单地说,就像一段话经过集体核准后在公告板上公示于众,一字不错不少,永久可见且无法涂改。

“上链”意味着“共识”和“存储”,两者缺一不可。交易不经过共识,则不能保证一致性和正确性,无法被链上所有参与者接受;共识后的数据不被多方存储,意味着数据有可能丢失或被单方篡改,更谈不上冗余可用。

除此之外,如果仅仅是调用接口查询一下,没有改变任何链上数据,也不需要进行共识确认,则不算“上链”。

或者,某个业务服务本身和区块链并不直接相关,或其业务流程无需参与共识,所生成的数据也不写入节点存储,那么这个业务服务称为“链下服务”,无论它是否和区块链节点共同部署在一台服务器,甚至和节点进程编译在一起。

当这个业务服务调用区块链的接口发送交易,且交易完成“共识”和“存储”后,才称为“上链”;如果这个交易没有按预期被打包处理,那么可以叫“上链失败”。

事实上,几乎所有的区块链系统,尤其是和实体经济、现实世界结合的区块链应用,都需要链上链下协同,用“混合架构“来实现,系统本身就包含丰富的技术生态。

*注1:交易(transaction)是区块链里的通用术语,泛指发往区块链,会改动链上数据和状态的一段指令和数据

*注2:本节描述的是简要的模型,在多层链、分片模型里,流程会更加复杂,事务划分更细,但“共识”和“存储”才叫上链的基本原则不变

交易之轻和“上链”之重

目前区块链底层平台逐步趋于成熟,性能和成本已经不是什么大问题,只是以下几个开销是因“分布式多方协作”而先天存在的:

  • 共识开销:主流共识算法里,PoW(工作量证明,也就是挖矿)消耗电力;PoS(权益证明)要抵押资产获得记账权;PBFT(联盟链常用的拜占庭容错算法)记账者要完成多次往返投票,流程步骤繁杂。
  • 计算开销:除了加解密、协议解析等计算之外,在支持智能合约的区块链上,为了验证合约的执行结果,所有节点都会无差别地执行合约代码,牵一发而动全身。
  • 网络开销:与节点数呈指数级比例,节点越多,网络传播次数越多,带宽和流量开销越大,如果数据包过大,就更雪上加霜。
  • 存储开销:和节点数成正比,所有的链上数据,都会写入所有节点的硬盘,在一个有100个节点的链上,就变成了100份副本,如果有1000个节点,那就是1000份。

也许有人会说:“这就是‘信任’的成本,值得的!”我同意。只是理想无法脱离现实,毕竟硬件资源总是有限的。

想象一下,如果每个交易都是一个复杂科学计算任务,那么每个节点CPU和内存会跑满;如果每个交易都包含一个大大的图片或视频,那么全网的带宽,以及各节点存储很快被塞爆;如果大家都敞开来滥用“链上”资源,“公地悲剧”就不可避免。

调用API发个交易是很容易的,而链上的开销就像房间里的大象,难以视而不见。作为开发者,需要正视“交易之轻和链上之重”,积极“上链”的同时减少不必要的开销,找到平衡之道。

*注1:常规联盟链节点参考配置:8核/16G内存/10m外网带宽/4T硬盘,不考虑“矿机”和其他特种配置。土豪随意,俗话说“钱能解决的问题都不是问题,问题是...”

*注2:本节暂未讨论“局部/分片共识”,也不探讨“平行扩容”的情况,默认假定全网参与共识和存储

让“链上”归链上,“链下”归链下

开销只是成本问题,而本质上,应该让区块链干自己最该干的事情。链上聚焦多方协作,尽快达成共识,营造或传递信任,将好钢用到刀刃上;那些非全局性的、无需多方共识的、数据量大的、计算繁杂的...通通放到链下实现,一个好汉三个帮。

如何进行切割?在业务层面,识别多方协作事务和数据共享中“最大公约数”,抓住要点痛点,四两拨千斤;在技术上,合理设计多层架构,扬长避短、因地制宜地运用多种技术,避免拿着锤子看什么都是钉子、一招打天下的思维。

为避免过于抽象,下面给出几个例子。

*注:每个例子其实都有大量的细节,考虑篇幅,这里做概要介绍,聚焦链上链下的区别和有机结合

文件能不能上链?

图片2.jpg

这是个非常高频的问题,经常被问到。这里的文件一般指图像、视频、PDF等,也可以泛指大体量的数据集,上链可信分享的目的,是使接受者可以验证文件的完整性、正确性。

常见的场景里,文件共享一般是局部的、点对点的,而不是广播给所有人,让区块链无差别地保存海量数据,会不堪重负。所以,合理的做法是计算文件的数字指纹(MD5或HASH),并与其他一些可选信息一起上链,如作者、持有人签名、访问地址等,单个上链信息并不多。

文件本身则保存在私有的文件服务器、云文件存储、或者IPFS系统里,这些专业方案更适合维护海量文件和大尺寸文件,容量更高、成本更低。注意,如果文件的安全级别到了“一个字节都不能泄露给无关人等”的程度,那么应慎用IPFS这种分布式存储的方案,优选私有存储方式。

需要分享文件给指定的朋友时,可以走专用传输通道点对点的发送文件,或者授权朋友到指定的URL下载,可以和区块链的P2P网络隔离,不占用区块链带宽。朋友获得文件后,计算文件的MD5、HASH,和链上对应的信息进行比对,验证数字签名,确保收到了正确且完整的文件。

这种方案,文件在链上“确权”、“锚定”和“寻址”,明文在链下传输并与链上互验,无论是成本、效率、还是隐私安全都取得了平衡。

怎么批量查询和分析数据?

图片3.jpg

对区块链上的数据进行分析是自然的需求,比如“某个账户参与哪些业务流程、完成了多少笔交易、成功率如何”,“某个记账节点在一段时间内参与了多少次区块记账、是否及时、有否作弊”,这些逻辑会牵涉到时间范围、区块高度、交易收发双方、合约地址、事件日志、状态数据等维度。

目前区块链底层平台一般是采用“Key-Value”的存储结构,其优势是读写效率极高,但难以支持复杂查询。

其次,复杂查询逻辑一般是在区块生成后进行,时效性略低,且并不需要进行多方共识,有一定的“离线”性。

最后,数据一旦“上链”,就不会改变,且只增不减,数据本身有明显特征(如区块高度、互相关联的HASH值、数字签名等)可以检验数据的完整性和正确性,在链上还是链下处理并无区别,任何拥有完整数据的节点都能支持独立的复杂查询。

于是,我们可以将数据完整地从链上导出,包括从创世块开始到最新的所有区块、所有交易流水和回执、所有交易产生的事件、状态数据等,通通写入链外的关系型数据库(如MySQL)或大数据平台,构建链上数据的“镜像”,然后可以采用这些引擎强大的索引模型、关联分析、建模训练、并行任务能力,灵活全面地对数据进行查询分析。

区块链浏览器、运营管理平台、监控平台、监管审计等系统,都会采用这种策略,链上出块,链下及时ETL入库,进行本地化地分析处理后,如需要和链上进行交互,再通过接口发送交易上链即可。

复杂逻辑和计算 

图片4.jpg

和复杂查询略有不同,复杂逻辑指交易流程中关系复杂、流程繁杂的部分。

如上所述,链上的智能合约会在所有节点上运行,如果智能合约写得过于复杂,或者包含其实不需要全网共识的多余逻辑,全网就会承担不必要的开销。极端的例子是,合约里写了个超级大的数据遍历逻辑(甚至是死循环),那么全网所有节点都会陷入这个遍历中,吭哧吭哧跑半天,甚至被拖死。

除了用类似GAS机制来控制逻辑的长度外,在允许的GAS范围内,我们推荐智能合约的设计尽量精简,单个合约接口里包含的代码在百行以上就算是比较复杂的了,可以考虑是否将一部分拆解出去。

拆解的边界因不同业务而异,颇为考验对业务的熟悉程度。开发者要对业务进行庖丁解牛式地分层分模块解耦,仅将业务流程中牵涉多方协作、需要共识、共享和公示的部分放到链上,使得合约只包含“必须”“铁定”要在链上运行的逻辑,合约逻辑“小而美”。

一般来说,多方见证的线上协同、公共账本管理、一定要分享给全体的关键数据(或数据的HASH)都是可以放到链上的,但相关的一些前置或后续的检验、核算、对账等逻辑可以适当拆解到链下。

一些和密集计算有关的逻辑,宜尽量将其在链下实现,如复杂的加解密算法,可以设计成链下生成证明链上快速验证的逻辑;如果业务流程中牵涉对各种数据的遍历、排序和统计,则在链下建立索引,链上仅进行Key-Value的精准读写。

其实,现在但凡看到合约里有用到mapping或array,我都会强迫症地想想能不能把这部分放链下服务去,个人比较欣赏“胖链下”和“瘦链上”的设计取向。

强调一下,精简链上合约逻辑,并不全是因为合约引擎的效率问题,合约引擎已经越来越快了。核心原因还是在发挥区块链最大功效的同时,避免“公地悲剧”。开发者拿出计算和存储成本最小的合约,有着“如无必要勿增实体”的奥卡姆剃刀式美感,更是对链上所有参与者表达尊重和负责任的态度。

即时消息:快速协商和响应

图片5.jpg

受队列调度、共识算法、网络广播等因素约束,“上链”的过程多少都会有一点延时。采用工作量证明共识的链,时延在十几秒到10分钟,采用DPOS、PBFT的共识,时延可缩短到秒级,此外,如果遇到网络波动、交易拥挤等特殊情况,时延表现会有抖动。

总的来说,对照毫秒或百毫秒级响应的瞬时交互,“上链”会显得些许“迟钝”。比如去超市买瓶水,支付后肯定不能站在那里等十几秒到十分钟,链出块确认后才走吧(略尴尬)。

对类似场景,宜结合链上预存和链外支付,在链下的点对点通道实现高频、快速、低延时的交易,链下确保收妥和响应,最后将双方的账户余额、交易凭据汇总到链上,在链上完成妥善记账。著名的“闪电网络”就类似这种模式。

另外,有些商业场景会先进行多轮的订单撮合、竞价拍卖或讨价还价。一般来说,这些操作是发生在局部的交易对手方之间,未必需要全网共识,所以也可以通过链下通道完成,最后将双方的订单(包含双方磋商结果、数字签名等信息)发送到链上,完成交易事务即可。

举个下快棋的例子,棋手的每一步棋并不需要实时上链,双方只管啪啪地下,裁判和观众只管围观,在棋局结束时,比如总共下了一百手,那么将这一百手的记录汇总起来,连同输赢结果上链,以便记录战绩分配奖金。如果要复盘棋局详情(如视频),可以参考上文提及的链下文件存储模式,用专用的服务器或分布式存储实现。

针对类似需求,在FISCO BCOS底层平台中,提供了AMOP(链上信使协议),利用已经搭建起来的区块链网络,在全网范围实现点对点、实时、安全的通信。基于AMOP,可以支持即时消息、快速协商、事件通知、交换秘密、构建私有交易等,推荐。

*注:【AMOP】详情可参考:

https://fisco-bcos-documentat..._CN/latest/docs/manual/amop_protocol.html

链下信息如何可信上链?

图片6.jpg

先看一个典型问题:“智能合约运行中要使用链外信息,怎么办?”

比如,链上有个世界杯决赛竞猜游戏,但世界杯不可能在链上踢吧;或者需要参考今天的天气,天气显然不是链上原生信息,应该从气象局获取;在跨境业务中,可能用到法定汇率,而汇率一定是来自权威机构的,不能在链上凭空生成。

这时候就要用到“预言机(Oracle)”,由一个或多个链下可信机构将球赛、天气、汇率等信息写到链上的公共合约,其他合约统一使用这份经过共识确认的可信信息,不会出现歧义。考虑到安全和效率,预言机(Oracle)会有多种具体做法,实现起来相当有趣。

图片7.jpg

更进一步的灵魂拷问是:如何保证上链的数据是真实的?”坦率地说,区块链并不能从根本上保证链下数据的可信性,只能保证信息一旦上链,就是全网一致且难以篡改的。而区块链跟实体经济结合时,势必要面对“如何可信上链”这个问题。

如资产相关应用,除了进行人员管理之外,还要“四流合一”,即“信息流、商流、物流、资金流”互相匹配和交叉印证,会使业务流程更加可信。这些“流”常常发生在链下现实世界,要把控它们,可能会用到物联网(传感器、摄像头等)、人工智能(模式识别、联邦学习等)、大数据分析、可信机构背书等多种技术和方式,这已经远远超出了区块链的范围。

所以,本节的命题其实是:区块链如何和数字世界里的技术广泛结合,更好地发挥自身多方协作、营造信任的作用

随着数字世界的发展、尤其“新基建”的强力推动,我们相信广泛的数字化能在保护隐私的前提下,降低信息采集和校验的成本,采集的数据会越来越丰富。

如在使用、转移、回收实体物资时,及时采集监测,甚至是多方、多路、多维度立体化的采集监控,并上链进行共识、公示、锚定,链上链下交叉验证,这样就可以逐渐逼近“物理世界可信上链”的效果,逻辑会更严密,更具有公信力,数据和价值流通会更可靠,协作的摩擦更低。

"链上"还是“链下"治理?

图片8.jpg

“治理”即制定行业联盟和业务运作规则,确保规则的执行,处理异常事件,奖励和惩戒参与者等。

以理想化的标准,似乎应该实现链上治理,通过代码决策、制定和执行规则,出错时系统具有“自修复”的“超能力"。实际上,完备的链上治理过于复杂,实现起来很有挑战性,尤其在需要达成现实世界法律法规的执行力时,纯链上的治理往往力不从心。

再多想一步:如完全依赖代码,万一代码本身有BUG、或者要“改需求”呢?链下的决策者、开发者如何发现和介入?

所以,“Code is Law”还是个理想化的目标,链下治理不可或缺。

联盟链参与者们组成管理委员会,在现实世界里进行民主集中制的讨论和决策,共同制定规则,采用多签、工作流的方式一起发起治理动作,调用区块链接口上链。

在链上,包括区块链底层平台和智能合约在内,都会内置一系列的决策和控制点,如支持多方投票决策,具备从业务层穿透到底层的准入和权限控制能力,可修改业务和节点的参数,能应对异常情况的重置账户,对错账进行冲正调账等等。

治理动作和结果经过共识确认,在链上全网生效,公开透明,接受广泛监督,彰显其合理性和公正性。必要时还可以引入监管方和司法仲裁。

反过来,联盟链上的数据,具备身份可知、难以篡改、无法否认且可全程追溯等特点,可为链下治理决策提供完备的数据基础,也便于为链下实际执行提供可信的凭据。所以,链上和链下有机结合,有助于设计完备、可控、可持续的治理机制。

如何做到“上” “下”自如

或许有人会说:“这链上链下什么的太复杂了,我就想用区块链!”

我认为这个说法很对。说到底,用户就想要一条趁手的“链”。作为开发者,我们要打造灵活的、插件化的系统架构,实现各种能力,什么数据导出、文件存储和传输、密集计算、数据采集和异步上链、治理监管、一键部署......按需取舍后,打包起来开箱即用,实际上提供了“基于区块链的一系列能力”。

最终呈现的“链”,除了节点之外,还有区块链浏览器、管理台、监控和审计系统、业务模板、APP/小程序等一系列交互入口,用户只需动动鼠标,点点页面,调调接口,一站式体验到一个完整的区块链应用。用户会觉得:“这就是区块链”,无需再分“链上”和“链下”,浑然一体。

说到这里,推荐一个我认为非常棒的设计:分布式身份标识(DID)。

DID是一套涵盖了分布式身份管理、可信数据交换的规范。权威机构为用户完成KYC,颁发凭据。用户将身份标识的摘要公布到链上,而将自己隐私数据存在链下(这一点非常重要)。

使用时,用户采用“明确授权”和“选择性披露”的策略,仅需出示少量的信息或加密证明,与链上数据进行对照校验,即可证明用户凭据和数据可信性,达成了“数据多跑路,用户少跑腿”、保护了用户隐私的可喜效果。

这种设计很好地将链上链下结合起来,逻辑闭环自洽,并不因为数据存在链下,就削弱了链上的功效,反而使得链的授信模型更为重要。

DID规范定义了语义清晰、层次分明的数据结构,以及通用的交互协议。开源项目WeIdentity完整地实现了DID协议,并提供丰富的周边支撑工具和服务,值得参考。

*注:【WeIdentity】详情可见:

https://fintech.webank.com/we...

结  语

链漫漫其修远兮,吾将“上下”而求索。在未来,“可信的”区块链将越来越多地和人们日常生活、实体经济联动,步入寻常百姓家。作为从业者,保持开放的心态,积极而创新地将区块链与更多技术结合,无论运作于链上还是链下,只要能解决问题、创造价值,就是一条好链。

分割线.jpg

FISCO BCOS的代码完全开源且免费下载地址↓↓↓
https://github.com/FISCO-BCOS...

图片9.jpg

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • SegmentFault 讲师
  • 获得 47 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • FISCO BCOS

    FISCO BCOS是开源联盟区块链底层技术平台,由金融区块链合作联盟(简称金链盟)成立开源工作组通力打造。成员包括博彦科技、华为、深证通、神州数码、四方精创、腾讯、微众银行、亦笔科技和越秀金科等金链盟成员机构。

注册于 2019-05-21
个人主页被 1.6k 人浏览