智能合约设计模式

2021-01-14 06:33:51
河北省科学院学报 2021年1期
关键词:修饰语状态变量设计模式

董 东

(河北师范大学 计算机与网络空间安全学院,河北 石家庄 050024)

0 引言

从Solidity语言的角度,智能合约(Smart Contract)是与以太坊区块链上特定地址绑定的代码(function)和数据(stateVariable)的集合[1]。智能合约的EBNF定义如下[2]:

<合约> := abstract? ( contract | interface | library ) <修饰符>

(is<继承修饰语> (,<继承修饰语> )* )?

{ <合约部件>* } ;

其中,

<合约部件> := <状态变量> | <用于> | <结构> | <修饰语> | <函数> |<事件> | <枚举>;

模式是某种事物的标准形式或使人可以照着做的标准形式。设计模式(Design Pattern)是在软件开发中针对普遍发生的问题而总结的被学术界、企业界和教育界认同的、反复使用的、经过分类编目的代码设计经验。设计模式是在特定环境下为解决某一通用软件设计问题提供的一套有效的解决方案。设计模式的应用提高了代码复用、可理解和可靠的程度。

面向合约设计模式包括四个要素:

名称(Pattern Name):每一个模式都有自己的名字,起到对该模式的标识作用,方便进行引用。

问题(Problem):在面向合约的系统设计过程中频繁出现的特定场景,比如“针对所有输入都是不可信的”,在函数中如何处理。面向合约的设计模式所解决的问题通常是与合约思维、安全、隐私等相关的问题。

解决方案(Solution):针对问题所设计的函数、修饰语、状态变量及其之间的关系,识别参与者和协作方式。

效果(Consequence):采用该模式对解决软件系统复杂性的影响。

应用设计模式为面向合约的软件开发能够提供如下帮助:(1)提高源代码的可读性和可理解性。由于设计模式提炼了针对一类场景的解决方案,具有普适性,所以源代码的阅读和理解变得更加容易;(2)降低对程序设计语言的学习成本。语言学习者在不同应用场景下和解决方案下理解语言中的保留字会更加有效率;(3)为代码审计(code audit)自动化提供途径[3]。代码审计是分析判断源代码是否符合正确性、安全性规范要求的过程。符合安全规范的设计模式为人工代码审计或自动化审计工具的研究提供了基础。

1 智能合约设计模式

通过对github (www.github.com)2019年更新的Solidity语言489 项目观察,初步识别了6个合约设计模式。

1.1 访问约束(Access Restriction)

由于区块链的公开性,无法完全保证合约的隐私。虽然在区块链中合约状态是公开访问的,但是可以通过另外的合约限制对某合约状态的读访问,比如约定某些情形下才可以调用合约函数或者只是允许授权访问等等。

在这个模式中有两个参与者:函数所属的合约和函数的调用者。函数的调用者可能是一个用户(user)或者是另外一个合约。被调用的函数以及相应的访问控制部件是模式的执行者。访问控制部件就是修饰语modifier。通过在修饰语的开始部分设置对约束条件的检查就可以实现对合约中函数的受限访问。下面是示例代码:

contract R {

//状态变量a在合约实例化时初始化为合约的创建者

address public a = msg.sender;

modifier onlyChangedBy(address _account) {

require(msg.sender == _account);

}

//通过把修饰语onlyChangedBy附加到函数setA上,使得只有该函数的调用者是合约的创建者时才能修改状态变量a

function setA(address _newA) public onlyChangedBy(a) {

a=_newA;

}

}

1.2 状态机(State Machine)

一个合约的生命周期是从其被创建到其被撤销的整个过程。这个过程如果可以划分为几个不同的阶段,并且阶段之间存在转移条件,那么自然地形成几个状态和若干状态转移,一般称为初始状态、结束状态和中间状态。在不同的状态下,合约的参与者能够执行不同的功能。重要的是,状态之间的转移是明确定义的、而且至少有一个参与者可以触发。

状态机模式的的参与者有两个:合约的创建者(owner)和与合约交互的访问者(user)。参与者能够直接或间接地引起合约状态转移。

实现状态机设计模式需要三个部件:状态的表示、函数的交互控制和状态的转移。枚举(Enum)是源代码中定义的类型,可以用来建模不同的状态。因为枚举可以显式地与整数类型进行转换,所以通过加1就能够转移到下一个状态,如果状态是线性的。通过访问约束模式应用修饰语实现第二个部件:限制某些函数只能在某状态下使用。只需设计函数修饰语,并附加在特定函数上,那么在执行被调用函数之前就会检查是否在制定状态下执行。如果某个函数执行完毕就应引起状态转移,则在该函数体末尾设计修饰语或者直接把新状态赋值给状态变量。下面的合约使用枚举定义了四个状态,使用修饰符定义了对状态检查,通过参数Stages.Running和修饰语at限定函数run只能在Stages.Running阶段执行。下面是示例代码:

contract S{

//定义四个状态

enum Stages { Ready, Running, Finalized, Finished }

//状态变量,指示当前状态

Stages public stage = Stages.Ready;

//判断当前状态是否是期望的状态

modifier at(Stages _stage) {

require(stage == _stage);

}

function run() public at(Stages.Running) {

//业务逻辑实现代码

}

}

访问约束和状态机设计模式在Solidity语言的官方文档中已经介绍。

1.3 前置条件模式(PreCondition)

函数执行所要满足的显式的或者隐式的条件称为函数的前置条件。可把require()作为函数体的初始语句,用于对本函数所处理的参数以及其它执行条件进行有效性检查,这种情况称为前置条件模式。下面是示例代码:

contract P {

function t(address addr) payable public {

//检查事务中用于转账的金额不能为0

require(msg.value != 0);

addr.transfer(msg.value);

}

}

1.4 后置条件模式(PostCondition)

函数执行后应当出现或者维持的一些条件称为函数执行的后置条件。当合约提出的需求被满足,合约自然生效,需求的提出方应自动按照合约规则进行满足后的运作。例如在一个关于某物品的拍卖合约中,一旦确定了最后的买家,则买家的钱将被自动转给拍卖者。在区块链中,应有某种机制保证合约逻辑按照预期进行。assert()通常作为函数体的结束语句,用于检查不变量和本函数的后置条件。下面是示例代码:

contract G {

function t(address addr) payable public {

uint balanceBeforeTransfer = this.balance;

uint transferAmount;

addr.transfer(transferAmount);

//确保转出后的余额等于转出前的余额减去转出金额

assert(this.balance == balanceBeforeTransfer-transferAmount);

}

}

通常情况下,assert()参数中的谓词总是为真。一旦不为真,整个转账事务回退,账户回到执行函数t之前的状态。这件事情由EVM来完成。

1.5 紧急制动(Emergency Stop)

即便是经过仔细的测试和严格的审计,智能合约中依然有存在缺陷和漏洞可能性。由于区块链的不变性(immutability)准则,一旦发现缺陷,但很难迅速修复代码。在完成修复之前,将暂停合约中至关重要的功能,就形成了紧急制动设计模式。在这个模式中有一个参与者:有权紧急制动的实体。通过一个指示合约是否处于制动状态的状态变量、一个具有授权修饰语的制动状态激活函数和一组由制动状态下禁止执行修饰语修饰的重要函数。下面是示例代码:

contract E {

//指示是否处于制动状态

bool isBraked = false;

modifier brakedInEmergency {

require(!isBraked);

}

modifier onlyWhenBraked {

require(isBraked);

}

modifier onlyAuthorized {

//检查msg.sender是否具有授权

//如require(msg.sender == owner)

}

//只有授权者才能调用

function brake() public onlyAuthorized {

isBraked = true;

}

//只有授权者才能调用

function resume() public onlyAuthorized {

isBraked = false;

}

//一旦制动,不可访问

function criticalOperation() public payable brakedInEmergency {

//关键函数的业务逻辑

}

function emergencyOperation() public onlyWhenBraked {

//紧急情况下的处理逻辑

}

}

1.6 代理(Proxy)

代理模式就是在合约访问者和合约提供者之间设置一个代理合约,访问者通过访问代理合约完成合约函数的调用。比如,由于合约的不变性,使得对合约的版本升级成为问题。在维持不变性的前提下,通过变通的方法可以实现版本升级。但是,新版本的合约地址将替换旧版本的合约地址。为了能够让合约的引用者使用新的地址,增加一个代理,让合约的访问者委托代理实现对合约的访问。当代理变更了新的合约地址后,就让所有对合约函数的访问变更到新地址上。实现时由让delegatecall函数把于在msg.data的前4个字节(包含了被调用函数的标识符)与新的地址绑定即可。为了让合约所有函数调用都能与新地址绑定,把delegatedcall函数放在合约的应变(fallback)函数中就能实现。应变函数是在合约收到不认识的函数调用时执行的函数。下面是示例代码:

contract P{

address delegate;

address owner = msg.sender;

//授权实体变更合约新地址

function upgrade(address newAddress) public {

require(msg.sender == owner);

delegate = newAddress;

}

//应变函数,处理upgrade以外的其它函数调用都触发应变函数执行

function() external payable {

assembly {

//目标地址

let _target := sload(0)

//复制函数签名和参数

calldatacopy(0x0, 0x0, calldatasize)

//在目标地址上执行函数调用

let result := delegatecall(gas, _target, 0x0, calldatasize, 0x0, 0)

//其它处理

}

}

}

2 研究展望

Donald Knuth教授说,设计程序是解释给人类让计算机做了什么的过程而不是告诉计算机做什么的过程[4]。程序员编写代码不仅仅是为了让计算机执行,也是为了将代码操作的精确细节传达给以后将对代码进行调整、更新、测试和维护的开发人员。学习和使用大多数程序员们为了解决某一问题而使用的代码片段对于提高程序质量和可理解性显得更加重要。

所以,设计模式一直是软件工程领域的热点话题。本文抽取了2019年以来的样本进行观察,识别了六种设计模式。随着Solidity语言版本迅速变化,个别模式的的实现设施可能会发生变化,但抽象结构不会改变。未来还需要做两方面工作:一是继续识别更多的设计模式,并使用适当的可视化方法进行表示。虽然智能合约也是对象,但有自己的特点,需要对UML类模型和记号进行扩展;二是设计Soidity语法分析器,形成大量源代码的中间表示,再使用是适当的机器学习收到进行模式识别。

猜你喜欢
修饰语状态变量设计模式
仿生设计模式的创新应用探索
玩具世界(2023年6期)2024-01-29 12:14:36
一阶动态电路零状态响应公式的通用拓展
基于TwinCAT3控制系统的YB518型小盒透明纸包装机运行速度的控制分析
类型学视野下英汉名词的修饰语功能研究反思
外国语文(2022年2期)2022-12-28 12:36:04
汉英名词前置修饰语顺序对比与汉语习得偏误研究*
“1+1”作业设计模式的实践探索
基于嵌套思路的饱和孔隙-裂隙介质本构理论
交通机电工程设计模式创新探讨
浩浩荡荡个什么
浩浩荡荡个什么