我們將要研究的應用程式是最流行的,令人興奮的剪刀石頭布(RPS)遊戲。這是一個有趣的例子,因為它很容易理解,同時還涉及一些豐富的狀態演化邏輯,以及在許多狀態通道應用程式中可能很重要的提交公開模式。為了讓您快速瞭解該應用程式,這裡有一個示例的gif:
狀態與獎勵措施
理解app的第一步是理解底層狀態。一輪RPS包含4種不同的狀態:
遊戲開始時兩個玩家都處於Resting狀態。然後Alice透過簽署一個提議狀態來提出一個回合,並將其傳送給Bob。該狀態包括他們將為之奮鬥的賭注,以及對Alice選擇出拳(在本例中為“石頭”)的預先承諾。然後Bob可以選擇是透過返回到Resting狀態來拒絕遊戲,或者透過簽署accept狀態來接受遊戲。如果他接受了,他會詳細說明他準備還擊的選擇。最後一步是由Alice來Reveal她的選擇,並以此作為勝利者,簽署展示步驟。正如你在下面的app definition contract部分中看到的,每個階段的轉換規則確保玩家按照規則進行遊戲(例如Accept->Reveal步驟只有在顯示的移動與預承諾匹配時才有效,依此類推)。
在提交展示階段,我們必須謹慎對待獎勵措施,如果狀態通道互動中斷了(例如,由於一方停止響應),那麼當前結果將在鏈上最終確定。因此我們必須確保在每個步驟上都能在鏈上獲得的結果不會激勵輪到其停止比賽的一方。
RPS中的風險點是步驟展示。當Alice傳送Accept狀態時,Bob就知道自己是否贏了。我們需要確保即使他輸了,他仍然有繼續下去的動力。我們的方法是將Accept狀態的結果設定為Bob已經輸掉了這一輪。這意味著停下來對鮑勃沒有好處,所以他最好繼續下去。
App definition contract
一旦我們設計了基本的狀態和激勵機制,下一步就是將智慧合約中的邏輯編碼成一種可以被狀態通道裁決者讀取的形式。為此,合約必須符合force-move app interface介面:
interface ForceMoveApp {
struct VariablePart {
bytes outcome;
bytes appData;
}
function validTransition(
VariablePart calldata a,
VariablePart calldata b,
uint256 turnNumB,
uint256 nParticipants
) external pure returns (bool);
}
目光敏銳的你們可能已經注意到,這與我們上週討論的介面略有不同,當時我們有validTransition(狀態s1,states2)。您在上面看到的版本是一個gas最佳化版本,我們去掉了一些不重要的狀態部分,並消除了turnNum和nParticipants的重複。
編寫應用程式合約的第一部分是定義狀態格式,以及將appData位元組反序列化為該格式的函式:
enum PositionType {Start, RoundProposed, RoundAccepted, Reveal}
enum Weapon {Rock, Paper, Scissors}
struct RPSData {
PositionType positionType;
uint256 stake; // the amount each player contributes to the round
bytes32 preCommit; // keccak(aWeapon, salt)
Weapon aWeapon; // player a's weapon
Weapon bWeapon; // player b's weapon
bytes32 salt;
}
function appData(bytes memory appDataBytes) internal pure returns (RPSData memory) {
return abi.decode(appDataBytes, (RPSData));
}
雖然不是所有的狀態都使用所有的屬性,但是我們在這裡採用了一種簡單的方法,即在結構中包含所有屬性,並且在未使用時將它們保留為空。我們正在使用solidity的新功能,但不再進行實驗性的abi-encoding函式,這使處理狀態通道狀態比以前更好了!
最後我們定義了validTransition函式本身:
function validTransition(
VariablePart memory fromPart,
VariablePart memory toPart,
uint256, /* turnNumB */
uint256 /* nParticipants */
) public pure returns (bool) {
// ... body omitted ...
}
這裡的完整程式碼相當簡單,但是太長了,無法在此處逐行進行。如果您有興趣,請檢視!https://github.com/statechannels/monorepo/blob/master/packages/rps/contracts/RockPaperScissors.sol#L51
使用狀態通道錢包
現在已經對應用程式定義合約進行了排序,接下來的任務是製作一個能夠允許使用者執行狀態通道應用程式的Web應用程式。從表面上看,這似乎很複雜。以下是一些需要涵蓋的內容:
1. 提供使用者介面以允許使用者選擇他們的動作
2. 根據使用者的選擇制定適當的狀態
3. 簽署這些狀態並將其傳送給對手
4. 儲存已傳送或已接收的任何狀態
5. 執行協議以開啟/資助/關閉/拒絕通道
6. 執行協議以發起挑戰(如果需要)
7. 監控鏈並應對挑戰
讓我們只關注上面的“signing states”部分,以瞭解這裡的挑戰。簽名資料基本上是每個以太坊應用程式的一個常見部分,並且有一個很好的模式來完成這一點:您將資料/交易傳送到您的eth錢包(例如MetaMask),它會提示使用者獲得許可,然後進行簽名。不難看出,這對狀態通道沒有多大用處。狀態通道的全部要點是能夠快速進行事務處理,每秒進行多次更新。如果每個簽名都必須透過使用者單擊來批准,則此方法將行不通。此外如果沒有定製的工具,使用者很難理解資料塊對他們簽名是否安全。
看起來,為了執行狀態通道應用程式,使用者需要一個能夠解釋狀態通道狀態的錢包。在某種程度上,錢包可以代表使用者自動對狀態進行簽名。將來普通的Eth錢包將具有這些功能,但是暫時我們需要一個特殊的狀態通道錢包來處理它們。
除了自動簽名交易外,該應用程式還可以將許多特定於狀態通道的職責轉移到狀態通道錢包中。在上面的列表中,狀態通道錢包可以使用3-7個,由應用程式負責UI並設計與應用程式相關的狀態。
狀態通道錢包儲存用於簽署狀態通道更新的金鑰,但在需要進行區塊鏈交易時可以到達以太坊錢包。使用者授予狀態通道錢包許可權,以代表他們簽署給定應用程式的狀態。此處的許可權可以是細粒度的,例如允許使用者限制總預算/支出率等。
該應用程式負責將訊息中繼給該通道中的其他參與者。這為應用程式開發人員提供了靈活性,使他們可以選擇滿足應用程式效能要求的傳輸層。這也開啟了將狀態通道狀態包括在現有訊息中的可能性,例如作為http請求上的自定義標頭。
在RPS演示應用中,狀態通道錢包被嵌入到iframe的頁面中,這意味著使用者無需單獨安裝任何軟體。這種方法在安全性和可靠性方面有一些缺點,因為狀態通道簽名金鑰和狀態都儲存在本地儲存中,但是這是一個合理的折衷方案,可以實現良好的入門流程。隨著使用者開始更頻繁地使用狀態通道,他們可能會想要安裝專用的狀態通道錢包應用程式。
應用程式設計模式
即使使用通道錢包管理簽名,儲存和狀態通道協議,我們仍然需要謹慎地在應用程式本身中跟蹤狀態通道的狀態。這是有道理的:通道錢包只能檢查狀態是否是有效的轉換;它不知道應用程式資料的含義,也不知道如何從給定的位置構造新的狀態。
一種有用的模式似乎是在應用程式本身內部快取狀態通道的當前狀態。然後在很大程度上基於通道的當前狀態,以狀態機樣式設計應用程式是很自然的。對於常見的應用程式(例如付款),合約開發人員可以提供定製的“渠道應用程式客戶端”,以免除應用程式開發人員的這一責任,並提供更多的“解決方案”開發體驗。
本文中的RPS應用程式基於以下狀態機。您會注意到,我們花了相當大的努力來掩蓋狀態圖中存在的玩家之間的潛在差異,以便為玩家提供統一的遊戲體驗:
該圖非常複雜,但希望也很容易解釋。您應該瞭解應用程式開發人員如何與狀態通道錢包進行互動,以及如何設計一個處理使用者輸入和對手觸發的狀態通道更新的應用程式。
下次
在下一篇文章中,我們將回到核心狀態通道協議,研究仲裁器的最佳化版本,以及如何使用稱為TLA +的正式驗證工具在協議層中查詢和修復安全漏洞。