本文为智能合约设计模式系列的一部分。
目的
确保合约不同状态暴露不同的功能
动机
合约的生命周期从初始状态开始,经历一些中间状态,到达最终状态。在不同状态下,合约以不同的方式运行,并提供不同的功能。拍卖、赌博、众筹等许多用例都反应了这点。即使Solidity官方文档也将其列为常见模式之一。状态转换有不同方式,有时状态在函数结尾转换,有时状态在指定时间段后转换。实现部分将做详细描述。
Gamma等人在1995年定义了类似功能的模式,但它的区块链版本有些不同。因为区块链本身就是一个状态转换系统,每个输入交易都会产生一个新状态。为了避免与区块链的状态混淆,我们定义合同的状态为阶段。
适用性
在以下条件时使用状态机模式
- 合约的生命周期需要经历不同的阶段
- 合约不同阶段具有不同的方法
- 阶段转换应当明确定义并对所有人公开
参与者和协作
状态机模式有两个参与者。一个是合约本身,其能够在不同阶段转换并确保每阶段中只提供对应的方法。另一个是合约所有者或使用者,他们可以初始化合约阶段并直接或间接的切换阶段。
实现
状态机模式的实现包括三个主要部分:阶段的表示、方法的访问控制以及阶段转换。
在Solidity中,可以使用枚举类型定义阶段。枚举是用户定义类型。先定义一个包含所有可能阶段的枚举,然后声明该枚举的变量存储当前阶段,并通过赋予其不同的阶段枚举值表示阶段转换。由于枚举可以显式地转化为整型,因此可以通过将阶段变量加1转换到下一阶段。
不同阶段的方法访问限制可以采用访问限制模式。在执行方法前,modifier会检查当前阶段是否为正确阶段,如果不为有效阶段,则使用守卫检查模式恢复事务。
有几种方式可以转换阶段。一种是在方法中转换,无论是专门的转换方法,还是处理业务逻辑的方法,转换本身就是流程的一部分。例如,轮盘赌合约,调用一个方法支付赢家收益,最后将阶段从GameEnded
转换为WinnersPaid
。此情况时,阶段转换可以是给阶段变量直接复制,或是利用 modifer 执行转化,亦或是调用helper方法。helper方法是个内部方法,每次调用阶段值就加1。另一个方式自动定时转换,合约保存一个阶段持续的时间或需要转换的某个未来时间点,相关方法的调用都会触发一个 modifer 检查当前时间点如果条件满足就转换阶段值。需要强调的是Solidity modifer 的顺序,阶段转换的 modifer 一定要在阶段检查的 modifer 之前,确保检查时阶段已经转换。
开发完成后,要进行足够的测试确保恶意调用不会触发意外的阶段转换,从而导致其获利或破坏合约。
代码示例
这个示例展示了盲拍合约的状态机,来源于Solidity官方文档示例代码。它包含了方法调用中转换和定时转换。由于完整合约比较复杂,此处只展示状态机相关代码,省略其它部分。
// This code has not been professionally audited, therefore I cannot make any promises about
// safety or correctness. Use at own risk.
contract StateMachine {
enum Stages {
AcceptingBlindBids,
RevealBids,
WinnerDetermined,
Finished
}
Stages public stage = Stages.AcceptingBlindBids;
uint public creationTime = now;
modifier atStage(Stages _stage) {
require(stage == _stage);
_;
}
modifier transitionAfter() {
_;
nextStage();
}
modifier timedTransitions() {
if (stage == Stages.AcceptingBlindBids && now >= creationTime + 6 days) {
nextStage();
}
if (stage == Stages.RevealBids && now >= creationTime + 10 days) {
nextStage();
}
_;
}
function bid() public payable timedTransitions atStage(Stages.AcceptingBlindBids) {
// Implement biding here
}
function reveal() public timedTransitions atStage(Stages.RevealBids) {
// Implement reveal of bids here
}
function claimGoods() public timedTransitions atStage(Stages.WinnerDetermined) transitionAfter {
// Implement handling of goods here
}
function cleanup() public atStage(Stages.Finished) {
// Implement cleanup of auction here
}
function nextStage() internal {
stage = Stages(uint(stage) + 1);
}
}
第5行定义了拍卖经历的四个阶段值。第12行定义阶段变量并赋予初始值。第14行定义了合约创建时间,定时转换会用到它。第16行定义了阶段检查 modifier ,合约必须处于入参的阶段,方法才可以执行。包含了transitionAfter
modifier 的方法,其结尾会调用内部方法nextStage
,转换到下一阶段。第26行定义定时转换 modifier ,比较当前时间点和预期的时间点以及当前阶段值,来决定是否转换阶段。
从第36行开始的4个外部方法只能在相应的阶段调用,这是由atStage
modifier 及其入参实现。前两个阶段是定时转换。注意,前3个方法包含了timedTransitions
modifier ,而不是前2个。这是因为真正的转换发生在方法调用时。例如,合约创建8天后调用bid
方法将转换到下一阶段,之后的atStage
modifier 将检测到阶段不匹配,并恢复整个事务包括阶段转换,这种情况中,timedTransitions
modifier 的作用是保证6天后无法调用bid
方法。阶段转换被持久化是发生在第一次调用下一阶段方法是,本例是调用reveal
方法,这时阶段检查通过,事务被执行。这种复杂的行为就是在实现部分提到的注意 modifier 顺序的原因。
第三阶段到第四阶段的转换由第44行包含的transitionAfter
modifier 完成。方法执行后,合约进入最后一个阶段,只允许调用cleanup
方法。
后果
应用此模式的一个后果是合约方法被划分到不同阶段,方法只能在指定阶段调用。此外,它还提供几种阶段转换的方式。
转换方式有一些需要注意的地方。定时转换给每个参与者带来明确策略的益处,但使用块编号或时间戳并不是完全没有风险。矿工可以在一定程度上控制块时间戳。因此,对于时间非常敏感的情况,应避免使用自动转换。如果考虑绝对安全,合约应在块时间戳偏离真是时间900秒的情况下保持健壮。此外,人工阶段转换也容易被合约所有人控制,他可以容易的转换合约状态,或放弃合约冻结所有资金。
已知应用
多个合约以某种形式应用此模式。一个例子是Ethorse合约,它可以对加密货币价格走势投注,阶段变量为结构体中的bool类型,当前阶段为true,自动转换阶段。另一个手动转换的例子是Pocketinns拍卖合约,它是一个社区驱动的市场生态系统。合约所有者有权自行改变阶段,尽管这被描述为紧急措施,但也为操控开启方便之门,此类合约的交易应谨慎进行。
推而广之
大部分智能合约天然的具有不同状态,去中心化应用处理的业务模型很多也有状态,例如一份订单、一只宠物、一个专利等。这也正是很多智能合约或去中心化应用可以采用此模式的原因。此模式类似传统设计模式中的状态模式,通过将合约生命周期划分为几个状态来简化合约的管理。
和状态模式不同的是,智能合约状态的主要作用是确保安全而非利于扩展。使用访问限制模式,实现状态检查方法,并在合约方法中(一般是开始)调用,同时在合约方法中(一般是最后)调用切换方法转换状态。
上述的自动定时转换在状态机中经常涉及。由于区块链合约确定性的特点,无法获取实时时间,而是当前区块时间(一般只有很少的差别)。EOS提供了now()和current_time()获得当前区块时间。HyperLedger Fabric则有ChaincodeStubInterface.GetTxTimestamp()方法。联盟链还可利用预言机模式定时触发智能合约切换状态。
完整内容请查看智能合约设计模式系列
网友评论