DApp 學習筆記 #1: Wrestling
在這個 Crypto Currency 潮流下,顯然作為一個會 JavaScript 的 自稱 工程師,學習一點智慧合約(Smart Contract)的開發也只是剛好而已! 這個系列將會簡單整理我最近在網路上或其他地方看到的一些資訊,並把過時的地方補上,所以 並不適合當作單一教學材料 喔!
這次挑選的文章是 @dev_zl 撰寫的 Ethereum Development Walkthrough 系列
我非常喜歡他在一開始對這些名詞做的一些梳理, 它首先使用 Ethereum 官方網站上的說明作為開頭:
Ethereum is a decentralized platform that runs smart contracts: applications that run exactly as programmed without any possibility of downtime, censorship, fraud or third-party interference.
翻譯過來就是:
乙太坊(Ethereum)是一個 用來執行智慧合約(Smart Contract)去中心化的平台:所有的應用程式都完全遵照程式碼執行,不會被關閉、不會被審查、不可能作弊,也不用擔心被其他人干擾。
接下來,他簡單的解釋了智慧合約的本質就是,一組用來控管金錢的手稿程式(Script,又譯腳本)
On Ethereum, Smart Contracts are scripts that can handle money. It’s as simple as that.
同時也推薦了很多相關的問答,可以很方便的釐清整個觀念,總之非常精彩,如果不排斥英文的話,請務必閱讀原作系列文第一篇!
簡介 Wrestling 合約
在開始之前,嚄想先簡單解釋一下 Wrestling 是什麼(因為我一開始還真的沒看懂 Orz)
首先,Wrestling 這個字直譯的話,是 摔角 的意思,我想是文化差異的關係,因為我完全想不出來這跟後面的程式有什麼關係…(再加上那個我看不懂的鐘點站比喻)
但是這個程式的概念其實很簡單易懂:
- 每回合開始時,兩個玩家分別把賭注放在一個箱子裡
- 如果任何一方放入的賭注總和超過對方的 2 倍,就獲勝
- 如果沒有一方獲勝,就進入下一回合
- 贏家可以拿走箱子裡所有的賭金
而這邊的箱子就是我們的 Wrestling 合約 / 程式,也可以理解為電腦控制的莊家。
撰寫 Wrestling 合約
撰寫 Ethereum 智慧合約的其中一個方法是,使用啟發自 JavaScript 的 Solidity 語言(除此之外還有Serpent、LLL、語法類似 Python 的 Viper 和 已經棄用的Mutan)也是原文中的作法,所以這裡僅是小幅修改程式,讓它符合新版的 Solidity 標準,並未更換語言。
constructor
以 Solidity 撰寫的合約的主體類似 JavaScript ES6 中的 class
,如果撰寫過的話應該會感到相當熟悉
contract ContractName {
constructor() public {
// 建構子的內容...
}
// 合約的其它部份
}
相比之下 JavaScript 大概長這樣子
class ClassName { constructor() { // 建構子的內容... } // 類別的其它部份... }
pragma
首先建立一個檔案 Wrestling.sol
並將以下的內容放進檔案
pragma solidity ^0.4.24;
contract Wrestling {
constructor() public {
}
}
這裡有一點要注意的是 pragma
這個註釋標籤,這個標籤在 Solidity 原始碼中被用來指定編譯器的最小版本,概念跟 npm
的 packages.json
裡面的相依套件版本很像(順帶一題,如果直接拿 pragma 去 Google 的話,會發現在 C/C++ 裡面,這是拿來餵前處理器的東西)
以上面的程式為例,我在撰寫這篇文章時的最新穩定版是 0.4.24,所以我選定這個版本為最小版本。加上 ^
的效果是選定這個版本以上,到下一個大更新之前(也就是 0.5.0
之前)
遊戲機制
加下來是這個程式的核心部份,也就是決定勝負的機制,用 JavaScript 來表示的話就大概長得像下面這樣:
function wrestle(player, bet) { if (gameFinished && !(player == wrestler1 || player == wrestler2)) { throw new Error() } if (player == wrestler1) { if (wrestler1Played) { throw new Error() } wrestler1Played = true; wrestler1Deposit = wrestler1Deposit + bet; } else /* if (player == wrestler2) */ { if (wrestler1Played) { throw new Error() } wrestler2Played = true; wrestler2Deposit = wrestler2Deposit + bet; } if (wrestler1Played && wrestler2Played) { if (wrestler1Deposit >= wrestler2Deposit * 2) { endOfGame(wrestler1); // wrestler1 wins!! } else if (wrestler2Deposit >= wrestler1Deposit * 2) { endOfGame(wrestler2); // wrestler2 wins!! } else { endOfRound(); } } }
可以說是非常簡單的程式碼,但是它並不能直接當作 Solidity 程式來執行(至少在這裡不行),我們需要做一些更改,首先是 throw Error
的部份,其實 Solidity 上並沒有這樣的功能,因為執行到一半突然中止的程式,可能會發生有些變數更改了,其它的卻還沒的狀況,管理並同步這些狀態會是一個大麻煩。
require
好消息是 Solidity 提供了一個 require()
功能,可以檢查輸入條件是否符合,如果不符合的話,就會取消這次的執行,並將程式到回到執行之前!
以這段程式碼來說:
if (gameFinished && !(player == wrestler1 || player == wrestler2)) { throw new Error() }
改寫之後會變成這樣:
require(!gameFinished && (player == wrestler1 || player == wrestler2));
其他幾個部份也是同理。另外,以 truffle console
為例,若是條件不符合的話,你就會看到類似下面的訊息:
truffle(development)> WrestlingInstance.wrestle({from: account1, value: web3.toWei(20, "ether")})
Error: VM Exception while processing transaction: revert
at XMLHttpRequest._onHttpResponseEnd (/usr/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:509:1)
at XMLHttpRequest._setReadyState (/usr/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:354:1)
at XMLHttpRequestEventTarget.dispatchEvent (/usr/lib/node_modules/truffle/build/webpack:/~/xhr2/lib/xhr2.js:64:1)
at XMLHttpRequest.request.onreadystatechange (/usr/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/httpprovider.js:128:1)
at /usr/lib/node_modules/truffle/build/webpack:/packages/truffle-provider/wrapper.js:134:1
at /usr/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/requestmanager.js:86:1
at Object.InvalidResponse (/usr/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/errors.js:38:1)
msg.sender
下一步是解決玩家如何加入的問題,因為會有二個玩家參與,所以加入二個變數用來儲存玩家的 address
資訊
address public wrestler1;
address public wrestler2;
然後是數值的傳遞,首先是玩家的加入,為了方便起見,我們假設拿出合約開始執行(物件化、new
)的人,就是第一個玩家(wrestler1
)
constructor() public {
wrestler1 = msg.sender;
}
上面的 msg.sender
就是當時與合約互動的帳戶(address
)
同理,第二個玩家則是使用 registerAsOpponent
這個 function 來加入遊戲
function registerAsAnOpponent() public {
require(wrestler2 == address(0));
wrestler2 = msg.sender;
}
至於這個 msg
是從哪裡來的,在 Solidity
的官方文件中,有著這麼一段解釋:
There are special variables and functions which always exist in the global namespace and are mainly used to provide information about the blockchain.
翻譯過來的話就是:
在 全域 空間(global namespace)裡面有一些特殊的 變數 和 方法,主要用來提供區塊鏈的訊息
簡單來說,在執行的時候,這些變數會被加到執行實體(instance)裡面,類似 bind
的概念;他們會隨時反映執行時的狀況,例如:
msg.sender
: 訊息的發送者(或這說是發出這個呼叫的人)msg.value
: 這個訊息包含了多少wei
個乙太幣now
: 主鏈現在的時間戳記- …等
msg.value
既然已經知道了 msg.value
是玩家發過來的乙太幣,那我們就知道該怎麼決定遊戲的勝負了,總之先把前面的 wrestle()
改寫一下:
function wrestle() public payable {
require(!gameFinished && (msg.sender == wrestler1 || msg.sender == wrestler2));
if (msg.sender == wrestler1) {
require(wrestler1Played == false);
wrestler1Played = true;
wrestler1Deposit = wrestler1Deposit + msg.value;
} else /* if (msg,sender == wrestler2) */ {
require(wrestler2Played == false);
wrestler2Played = true;
wrestler2Deposit = wrestler2Deposit + msg.value;
}
if (wrestler1Played && wrestler2Played) {
if (wrestler1Deposit >= wrestler2Deposit * 2) {
endOfGame(wrestler1);
} else if (wrestler2Deposit >= wrestler1Deposit * 2) {
endOfGame(wrestler2);
} else {
endOfRound();
}
}
}
然後把獲勝之後的處理寫成一個方法:
function endOfGame(address winner) internal {
gameFinished = true;
theWinner = winner;
gains = wrestler1Deposit + wrestler2Deposit;
}
還有每回合結束之後的動作:
function endOfRound() internal {
wrestler1Played = false;
wrestler2Played = false;
}
Ps. 所有收取乙太幣的方法之後都要加上 payable
,不然會收不到錢喔!
public? private?
在上面的程式碼中,我們有一個部份沒有解釋,那就是變數名稱前面,和方法名稱後面的 private
,如果經撰寫過 Java 或其它類似語言的話,應該會立刻想到裡面的存取修飾子,實際上,兩者概念非常相似,但有一個很重要的區別必須要記住:
Everything that is inside a contract is visible to all external observers. Making something
private
only prevents other contracts from accessing and modifying the information, but it will still be visible to the whole world outside of the blockchain.
翻譯成中文的話大概是:
智慧合約中的所有內容對外界來說都是可見的。就算設定成
private
也不會阻止其他人阻止讀取裡面的內容,僅僅是在區塊鏈上執行時,免於其他合約讀取和修改其中的內容而已
至於為什麼會這樣?因為它是區塊鏈啊!所有的內容都會被同步到各個節點上,而且所有的內容都可以被驗證,那當然要可以被讀取啊!
withdraw
贏錢之後,總還是要想辦法提出來的,所以我們最後一件事就是使用 transfer()
把合約裡面的錢轉給獲勝者:
function withdraw() public {
require(gameFinished && theWinner == msg.sender);
uint amount = gains;
gains = 0;
msg.sender.transfer(amount);
}
這裡的 transfer()
是 address 上的一個方法,可以用來將指定的金額轉入該帳戶(單位是 wei
)
完整的程式碼
pragma solidity ^0.4.24;
contract Wrestling {
address public wrestler1;
address public wrestler2;
bool wrestler1Played;
bool wrestler2Played;
uint private wrestler1Deposit;
uint private wrestler2Deposit;
bool public gameFinished;
address public theWinner;
uint gains;
constructor() public {
wrestler1 = msg.sender;
}
function registerAsAnOpponent() public {
require(wrestler2 == address(0));
wrestler2 = msg.sender;
}
function wrestle() public payable {
require(!gameFinished && (msg.sender == wrestler1 || msg.sender == wrestler2));
if (msg.sender == wrestler1) {
require(wrestler1Played == false);
wrestler1Played = true;
wrestler1Deposit = wrestler1Deposit + msg.value;
} else /* if (msg,sender == wrestler2) */ {
require(wrestler2Played == false);
wrestler2Played = true;
wrestler2Deposit = wrestler2Deposit + msg.value;
}
if (wrestler1Played && wrestler2Played) {
if (wrestler1Deposit >= wrestler2Deposit * 2) {
endOfGame(wrestler1);
} else if (wrestler2Deposit >= wrestler1Deposit * 2) {
endOfGame(wrestler2);
} else {
endOfRound();
}
}
}
function endOfRound() internal {
wrestler1Played = false;
wrestler2Played = false;
}
function endOfGame(address winner) internal {
gameFinished = true;
theWinner = winner;
gains = wrestler1Deposit + wrestler2Deposit;
}
function withdraw() public {
require(gameFinished && theWinner == msg.sender);
uint amount = gains;
gains = 0;
msg.sender.transfer(amount);
}
}
注: Solidity 的圖示屬於 Ethereum Foundation