本文為智能合約設(shè)計模式系列的一部分。
目的
確保合約不同狀態(tài)暴露不同的功能
動機
合約的生命周期從初始狀態(tài)開始蜀变,經(jīng)歷一些中間狀態(tài)宣决,到達(dá)最終狀態(tài)。在不同狀態(tài)下昏苏,合約以不同的方式運行尊沸,并提供不同的功能。拍賣贤惯、賭博洼专、眾籌等許多用例都反應(yīng)了這點。即使Solidity官方文檔也將其列為常見模式之一孵构。狀態(tài)轉(zhuǎn)換有不同方式屁商,有時狀態(tài)在函數(shù)結(jié)尾轉(zhuǎn)換,有時狀態(tài)在指定時間段后轉(zhuǎn)換颈墅。實現(xiàn)部分將做詳細(xì)描述蜡镶。
Gamma等人在1995年定義了類似功能的模式,但它的區(qū)塊鏈版本有些不同恤筛。因為區(qū)塊鏈本身就是一個狀態(tài)轉(zhuǎn)換系統(tǒng)官还,每個輸入交易都會產(chǎn)生一個新狀態(tài)。為了避免與區(qū)塊鏈的狀態(tài)混淆毒坛,我們定義合同的狀態(tài)為階段望伦。
適用性
在以下條件時使用狀態(tài)機模式
- 合約的生命周期需要經(jīng)歷不同的階段
- 合約不同階段具有不同的方法
- 階段轉(zhuǎn)換應(yīng)當(dāng)明確定義并對所有人公開
參與者和協(xié)作
狀態(tài)機模式有兩個參與者。一個是合約本身煎殷,其能夠在不同階段轉(zhuǎn)換并確保每階段中只提供對應(yīng)的方法屯伞。另一個是合約所有者或使用者,他們可以初始化合約階段并直接或間接的切換階段豪直。
實現(xiàn)
狀態(tài)機模式的實現(xiàn)包括三個主要部分:階段的表示劣摇、方法的訪問控制以及階段轉(zhuǎn)換。
在Solidity中弓乙,可以使用枚舉類型定義階段末融。枚舉是用戶定義類型。先定義一個包含所有可能階段的枚舉唆貌,然后聲明該枚舉的變量存儲當(dāng)前階段滑潘,并通過賦予其不同的階段枚舉值表示階段轉(zhuǎn)換。由于枚舉可以顯式地轉(zhuǎn)化為整型锨咙,因此可以通過將階段變量加1轉(zhuǎn)換到下一階段。
不同階段的方法訪問限制可以采用訪問限制模式追逮。在執(zhí)行方法前酪刀,modifier會檢查當(dāng)前階段是否為正確階段粹舵,如果不為有效階段,則使用守衛(wèi)檢查模式恢復(fù)事務(wù)骂倘。
有幾種方式可以轉(zhuǎn)換階段眼滤。一種是在方法中轉(zhuǎn)換,無論是專門的轉(zhuǎn)換方法历涝,還是處理業(yè)務(wù)邏輯的方法诅需,轉(zhuǎn)換本身就是流程的一部分。例如荧库,輪盤賭合約堰塌,調(diào)用一個方法支付贏家收益,最后將階段從GameEnded
轉(zhuǎn)換為WinnersPaid
分衫。此情況時场刑,階段轉(zhuǎn)換可以是給階段變量直接復(fù)制,或是利用 modifer 執(zhí)行轉(zhuǎn)化蚪战,亦或是調(diào)用helper方法牵现。helper方法是個內(nèi)部方法,每次調(diào)用階段值就加1邀桑。另一個方式自動定時轉(zhuǎn)換瞎疼,合約保存一個階段持續(xù)的時間或需要轉(zhuǎn)換的某個未來時間點,相關(guān)方法的調(diào)用都會觸發(fā)一個 modifer 檢查當(dāng)前時間點如果條件滿足就轉(zhuǎn)換階段值壁畸。需要強調(diào)的是Solidity modifer 的順序丑慎,階段轉(zhuǎn)換的 modifer 一定要在階段檢查的 modifer 之前,確保檢查時階段已經(jīng)轉(zhuǎn)換瓤摧。
開發(fā)完成后竿裂,要進(jìn)行足夠的測試確保惡意調(diào)用不會觸發(fā)意外的階段轉(zhuǎn)換,從而導(dǎo)致其獲利或破壞合約照弥。
代碼示例
這個示例展示了盲拍合約的狀態(tài)機腻异,來源于Solidity官方文檔示例代碼。它包含了方法調(diào)用中轉(zhuǎn)換和定時轉(zhuǎn)換这揣。由于完整合約比較復(fù)雜悔常,此處只展示狀態(tài)機相關(guān)代碼,省略其它部分给赞。
// 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行定義了拍賣經(jīng)歷的四個階段值机打。第12行定義階段變量并賦予初始值。第14行定義了合約創(chuàng)建時間片迅,定時轉(zhuǎn)換會用到它残邀。第16行定義了階段檢查 modifier ,合約必須處于入?yún)⒌碾A段,方法才可以執(zhí)行芥挣。包含了transitionAfter
modifier 的方法驱闷,其結(jié)尾會調(diào)用內(nèi)部方法nextStage
,轉(zhuǎn)換到下一階段空免。第26行定義定時轉(zhuǎn)換 modifier 空另,比較當(dāng)前時間點和預(yù)期的時間點以及當(dāng)前階段值,來決定是否轉(zhuǎn)換階段蹋砚。
從第36行開始的4個外部方法只能在相應(yīng)的階段調(diào)用扼菠,這是由atStage
modifier 及其入?yún)崿F(xiàn)。前兩個階段是定時轉(zhuǎn)換坝咐。注意循榆,前3個方法包含了timedTransitions
modifier ,而不是前2個畅厢。這是因為真正的轉(zhuǎn)換發(fā)生在方法調(diào)用時冯痢。例如,合約創(chuàng)建8天后調(diào)用bid
方法將轉(zhuǎn)換到下一階段框杜,之后的atStage
modifier 將檢測到階段不匹配浦楣,并恢復(fù)整個事務(wù)包括階段轉(zhuǎn)換,這種情況中咪辱,timedTransitions
modifier 的作用是保證6天后無法調(diào)用bid
方法振劳。階段轉(zhuǎn)換被持久化是發(fā)生在第一次調(diào)用下一階段方法是,本例是調(diào)用reveal
方法油狂,這時階段檢查通過历恐,事務(wù)被執(zhí)行。這種復(fù)雜的行為就是在實現(xiàn)部分提到的注意 modifier 順序的原因专筷。
第三階段到第四階段的轉(zhuǎn)換由第44行包含的transitionAfter
modifier 完成弱贼。方法執(zhí)行后,合約進(jìn)入最后一個階段磷蛹,只允許調(diào)用cleanup
方法吮旅。
后果
應(yīng)用此模式的一個后果是合約方法被劃分到不同階段,方法只能在指定階段調(diào)用味咳。此外庇勃,它還提供幾種階段轉(zhuǎn)換的方式。
轉(zhuǎn)換方式有一些需要注意的地方槽驶。定時轉(zhuǎn)換給每個參與者帶來明確策略的益處责嚷,但使用塊編號或時間戳并不是完全沒有風(fēng)險。礦工可以在一定程度上控制塊時間戳掂铐。因此罕拂,對于時間非常敏感的情況揍异,應(yīng)避免使用自動轉(zhuǎn)換。如果考慮絕對安全聂受,合約應(yīng)在塊時間戳偏離真是時間900秒的情況下保持健壯蒿秦。此外烤镐,人工階段轉(zhuǎn)換也容易被合約所有人控制蛋济,他可以容易的轉(zhuǎn)換合約狀態(tài),或放棄合約凍結(jié)所有資金炮叶。
已知應(yīng)用
多個合約以某種形式應(yīng)用此模式碗旅。一個例子是Ethorse合約,它可以對加密貨幣價格走勢投注镜悉,階段變量為結(jié)構(gòu)體中的bool類型祟辟,當(dāng)前階段為true,自動轉(zhuǎn)換階段侣肄。另一個手動轉(zhuǎn)換的例子是Pocketinns拍賣合約旧困,它是一個社區(qū)驅(qū)動的市場生態(tài)系統(tǒng)。合約所有者有權(quán)自行改變階段稼锅,盡管這被描述為緊急措施吼具,但也為操控開啟方便之門,此類合約的交易應(yīng)謹(jǐn)慎進(jìn)行矩距。
推而廣之
大部分智能合約天然的具有不同狀態(tài)拗盒,去中心化應(yīng)用處理的業(yè)務(wù)模型很多也有狀態(tài),例如一份訂單锥债、一只寵物陡蝇、一個專利等。這也正是很多智能合約或去中心化應(yīng)用可以采用此模式的原因哮肚。此模式類似傳統(tǒng)設(shè)計模式中的狀態(tài)模式登夫,通過將合約生命周期劃分為幾個狀態(tài)來簡化合約的管理。
和狀態(tài)模式不同的是允趟,智能合約狀態(tài)的主要作用是確保安全而非利于擴展恼策。使用訪問限制模式,實現(xiàn)狀態(tài)檢查方法拼窥,并在合約方法中(一般是開始)調(diào)用戏蔑,同時在合約方法中(一般是最后)調(diào)用切換方法轉(zhuǎn)換狀態(tài)。
上述的自動定時轉(zhuǎn)換在狀態(tài)機中經(jīng)常涉及鲁纠。由于區(qū)塊鏈合約確定性的特點总棵,無法獲取實時時間,而是當(dāng)前區(qū)塊時間(一般只有很少的差別)改含。EOS提供了now()和current_time()獲得當(dāng)前區(qū)塊時間情龄。HyperLedger Fabric則有ChaincodeStubInterface.GetTxTimestamp()方法。聯(lián)盟鏈還可利用預(yù)言機模式定時觸發(fā)智能合約切換狀態(tài)。
完整內(nèi)容請查看智能合約設(shè)計模式系列