剪刀石头布dapp开发

1. 实验任务和需求

开发一个剪刀石头布的智能合约,实现以下功能或特性:

(1)实现剪刀石头布游戏的基本逻辑,正确判断游戏结果。

(2)将玩家提交的信息加密(暂且将取hash作为加密)储存在区块中,尽可能避免作弊。

(3)尽可能保证合约在输入非法的情况下仍能运行。

(4)使合约具有一定的可复用性。

并开发与之配套的前端程序,使合约易于使用。

2. 概要设计(实验思路)

(1)使用bet函数判断用户能否加入游戏,如果可以则保存用户的地址与提交的hash

(2)使用reveal函数接收用户对自己选择的披露并进行校验

(3)使用getreward函数给用户发放奖励

3. 详细设计(关键算法)

(1)全局数据结构:

address payable[2] player;//当前玩家地址

bytes32[2] p;//hash加密下注情况

uint[2] value;//下注金额

uint8[2] chosed;//玩家选择手势,1:剪刀 2石头 3布

uint8 numberOfPlayer;//当前玩家数量

uint8 winner;//2:未决定,3:平局,1:玩家1,0:玩家0

uint timeout;//超时时间

uint finishtime;//玩家完成提交hash时间

uint MinValue;//最小下注金额

(2)只有当玩家数量不足或上一局玩家超时过长(4倍timeout)时,bet函数才会允许新玩家加入。如果情况属于后者,则还会将下注金额分别退还给上一局两位玩家,并进行重置。同时,bet函数不允许玩家对提交内容进行修改,与现实较为一致。

(3)reveal函数对输入取hash,并与bet保存的hash比较,如果一致,则储存输入。

(4)getwinner函数判断胜者,此函数中硬编码了所有合法的情况,并会将其他情况判定为平局。

(5)getreward函数首先判断是否达到获取奖励的条件,即是否游戏双方都完成了披露、调用者是否为胜者,然后进行先更改余额再进行转账并重置合约。如果平局,则双方可分别拿回自己的下注。如果超时且仅有一方披露,则认为该方为胜者。如果超时且两方均未披露,则认为平局。

(6)reset用于重置合约状态,设置为internal以防止外部恶意调用

generatehash用于创建执行bet函数所需的hash,设置为pure,执行情况不会被记录

getinfo用于获取合约状态,主要用于前端展示,设置为view

user用于获取玩家编号(0,1),非当前玩家调用会导致错误

4. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
pragma solidity ^0.5.0;
/// @title 猜拳合约
/// @author sry
/// @dev 仅在有限的测试中没有发生错误
contract putETH {
address payable[2] player;//玩家地址
bytes32[2] p;//hash加密下注情况
uint[2] value=[0,0];//下注金额
uint8[2] chosed=[0,0];//1:剪刀 2石头 3布
uint8 numberOfPlayer = 0;
uint8 winner=2;//2:未决定,3:平局,1:玩家1,0:玩家0
uint timeout=30 minutes;
uint finishtime=now;
uint MinValue=1 wei;
/// @notice 用户下注
/// @param hash 用户下注信息,传入应当为keccak256(abi.encodePacked(uint8 choose,uint secret))
/// @return hash
function bet(bytes32 hash) public payable returns(bytes32){
if(now>=finishtime+4*timeout && msg.sender!=player[0]&&msg.sender!=player[1]){
uint refund;
refund=value[0];
value[0]=0;
player[0].transfer(refund);
refund=value[1];
value[1]=0;
player[1].transfer(refund);
reset();
}
if (numberOfPlayer>=2)revert();
if (msg.value<MinValue)revert();
if (numberOfPlayer==1 && msg.sender==player[0])revert();
numberOfPlayer=numberOfPlayer+1;
value[numberOfPlayer-1]=msg.value;
p[numberOfPlayer-1]=hash;
player[numberOfPlayer-1]=msg.sender;
finishtime=now;
return hash;
}
/// @notice 用户披露下注情况
/// @param choose 用户选择的手势
/// @param secret 用户自定密钥
/// @return choose
function reveal(uint8 choose,uint secret) external returns(uint){
uint8 user=user(msg.sender);
if (p[user]==keccak256(abi.encodePacked(choose, secret))) {
chosed[user]=choose;
}else revert();
return choose;
}
/// @notice 获取奖励,平局分别取回
/// @return reward 奖励值
function GetReward() external returns(uint){
uint reward=0;
if((chosed[0]==0 || chosed[1]==0) && now<finishtime+timeout){
revert();
}
if(now>=finishtime+timeout && (chosed[0]==0 || chosed[1]==0)){
uint8 user=user(msg.sender);
if(chosed[0]==0 && chosed[1]==0){
winner=3;
}
else{
if(chosed[user]==0)revert();
reward=value[0]+value[1];
value[0]=0;
value[1]=0;
player[user].transfer(reward);
reset();
return reward;}
}
if(winner==2)winner=getwinner();
require(winner!=2);
if(winner!=3){
if(msg.sender!=player[winner])revert();
reward=value[0]+value[1];
value[0]=0;
value[1]=0;
player[winner].transfer(reward);
reset();
}else {
uint8 user=user(msg.sender);
reward=value[user];
value[user]=0;
msg.sender.transfer(reward);
if(value[0]==0 && value[1]==0)reset();
}
return reward;
}
/// @notice 计算胜者
/// @dev reveal()并没有检查choose的值是否合法,在此函数中,不合法的输入会被判定为平局
/// @return user 胜者编号
function getwinner() public view returns(uint8){
if(chosed[0]==chosed[1]){
return 3;
}
else if((chosed[0]==2 && chosed[1]==1) || (chosed[0]==1 && chosed[1]==3) || (chosed[0]==3 && chosed[1]==2)){
return 0;
}
else if((chosed[1]==2 && chosed[0]==1) || (chosed[1]==1 && chosed[0]==3) || (chosed[1]==3 && chosed[0]==2)){
return 1;
}
return 3;
}
/// @notice 重置
function reset() internal {
numberOfPlayer=0;
chosed[0]=0;
chosed[1]=0;
winner=2;
finishtime=now;
}
/// @notice 获取用户编号
/// @dev 调用用户非当前玩家会导致revert
/// @param t 用户地址
/// @return user 用户编号
function user(address t) internal view returns(uint8){
if(t==player[0]){return 0;}
else if(t==player[1]){return 1;}
else revert();
}
/// @notice 获取当前信息
/// @return player,value,chosed
function GetInfo() public view returns(address,address,uint,uint,uint8,uint8){
return (player[0],player[1],value[0],value[1],chosed[0],chosed[1]);
}
/// @notice 生成hash
/// @dev 函数声明为pure,执行情况不会被记录
/// @param choose 选择手势
/// @param secret 密码
/// @return x 生成的hash
function GenerateHash(uint8 choose,uint secret) pure public returns(bytes32){
bytes32 x;
x=keccak256(abi.encodePacked(choose,secret));
return x;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<style>
/*--预设--*/
body { padding:0px;margin: 0px; }
#lyrow, #lyrow input, #lyrow textarea { font-size:12px;font-family: 'Microsoft YaHei', '微软雅黑', MicrosoftJhengHei, '华文细黑', STHeiti, MingLiu; }
#lyrow { height:100vh;width: 100vw; }
#lyrow div { min-height: 18px; }
#lyrow input, #lyrow textarea { border:rgb(235, 235, 235) 1px solid;border-radius: 3px;padding: 5px 8px;outline: 0; }
#lyrow input:hover, #lyrow textarea:hover { border: 1px solid #6bc1f2; }
/*--编辑--*/

</style>

<div id="lyrow">

<input type="submit" name="button" value="连接metamask" id="connect" >
<span >选择手势:</span>
<select name="select" id="choose" >
<option value="1">剪刀</option>
<option value="2">石头</option>
<option value="3"></option>
</select>
<span >选择账号:</span>
<select name="select" id="acc" >

</select>
<span >下注金额(eth,支持小数):</span>
<input type="text" name="input" oninput="value=value.replace(/[^\d.]/g,'')" id="val" value="1">
<span >随机数:</span>
<input type="text" name="input" oninput="value=value.replace(/[^\dabcdefx]/g,'')" id="ran">
<input type="submit" name="button" value="获取当前状态" id="getinfo" >
<input type="submit" name="button" value="下注" id="bet" >
<input type="submit" name="button" value="披露下注情况" id="reveal" >
<input type="submit" name="button" value="查看结果" id="getwinner" >
<input type="submit" name="button" value="获取奖励" id="getreward" >
</div>
<div id="player-content">


</div>
<script src="js/web3.min.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/index.js"></script>

</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
$(function() {
var account = null;
var web3 = null;
const address = '0xb92b82a94382b9110e8febBEd4AEfB7F3436a26F';//'0xa7228a391b6B0E343C33F6aE5A511Eefaf540688';//'0xd6C500DccEF6B65eF956B7446a405cbed4c47814' ;// 合约部署地址
var contract =null;
var choose=0;
var r=null;
const abi =[
{
"constant": true,
"inputs": [
{
"internalType": "uint8",
"name": "choose",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "secret",
"type": "uint256"
}
],
"name": "GenerateHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "GetInfo",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "GetReward",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "hash",
"type": "bytes32"
}
],
"name": "bet",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getwinner",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint8",
"name": "choose",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "secret",
"type": "uint256"
}
],
"name": "reveal",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
];
function handleAccountsChanged(accounts) {
var acc=document.getElementById("acc");
if (accounts.length === 0) {
console.log('Please connect to MetaMask.');
} else{
acc.innerHTML="";
for (var a=0; a<accounts.length;a++){
acc.innerHTML+=`<option value="${accounts[a]}">${accounts[a]}</option>\n`;
};
account = acc.value;
console.log(account);
}
}
function changeaccount(){
var acc=document.getElementById("acc");
account = acc.value;
console.log(account);
}

function connect(){
ethereum
.request({ method: 'eth_requestAccounts' })
.then(handleAccountsChanged)
.catch((err) => {
if (err.code === 4001) {
console.log('Please connect to MetaMask.');
} else {
console.error(err);
}
});
web3 = new Web3(window.web3.currentProvider);
contract= new web3.eth.Contract(abi, address);
}
function bet(){
var sel=document.getElementById("choose");
var val=document.getElementById("val");
var ran=document.getElementById("ran");
var hash;
web3.utils.randomHex(32);
contract.methods.GenerateHash(sel.value,parseInt(ran.value)).call().then(function(result) {
hash=result;//web3.utils.hexToAscii(result);
console.log(hash);
contract.methods.bet(hash).send({
from: account,gasPrice: "1000000000",value: String(parseFloat(val.value)*1000000000000000000)
})
.on('error', function(error, receipt) {alert(error,receipt);return;})
.on('receipt', (data) => {
console.log(data);

});
});
sel.disabled=true;
val.disabled=true;
ran.disabled=true;

}
function reveal(){
var sel=document.getElementById("choose");
var val=document.getElementById("val");
var ran=document.getElementById("ran");
contract.methods.reveal(sel.value,parseInt(ran.value)).send({from: account,gasPrice: "1000000000"})
.on('error', function(error, receipt) {alert(error,receipt);return;})
.then(function(result) {
console.log(result);
});
sel.disabled=false;
val.disabled=false;
ran.disabled=false;
}
function getreward(){
var sel=document.getElementById("choose");
contract.methods.GetReward().send({from: account,gasPrice: "1000000000"})
.on('error', function(error, receipt) {alert(error,receipt);return;})
.then(function(result) {
console.log(result);
});
}
function getinf(){
var res=["have not revealed","剪刀","石头","布"];
contract.methods.GetInfo().call().then(function(result) {
console.log(result);
alert("player 0:"+result[0]+"\n"+
"player 1:"+result[1]+"\n"+
"value 0:"+result[2]+"\n"+
"value 1:"+result[3]+"\n"+
"choose 0:"+res[parseInt(result[4])]+"\n"+
"choose 1:"+res[parseInt(result[5])]+"\n"
);
});
}
function getwinner(){
var res=["player 0 wins","player 1 wins","have not decided","draw/other situations"];
contract.methods.getwinner().call().then(function(result) {
alert(res[parseInt(result)]);
});
}
$("#connect").on('click', connect);
$("#getinfo").on('click', getinf);
$("#bet").on('click', bet);
$("#reveal").on('click', reveal);
$("#getwinner").on('click', getwinner);
$("#getreward").on('click', getreward);
$("#acc").on('change', changeaccount);
document.getElementById("ran").value=Web3.utils.randomHex(32);
})

5. 测试方案

创建两个账户模拟两个玩家进行游戏,注意完全模拟输赢和平局情况,并模拟一些非法输入或超时的情况。创建第三个账户,模拟非玩家调用情况。将合约部署至测试链,将前端部署至服务器,测试前端工作情况。

6. 测试过程

先在remix vm中进行测试。

(secret的类型为uint256,实际前端会随机生成32byte作为secret,足够安全。为了测试方便使用较简单的secret)

生成hash:

下注及执行后情况:

披露:

对另一账号进行相似操作,得结果:

(超时处理的效果难以展示,从略)

7. 结果分析

在有限的测试中,合约和前端都正常工作。合约能够正确执行游戏逻辑,并具有一定的安全性和抵抗非法输入的能力。前端能够与合约恰当配合,具有一定的易用性。

8. 总结

本实验设计了一个剪刀石头布的智能合约,并配套开发了前端页面,且进行了相关测试,基本完成了实验的目标与需求。

但是受限于开发时间、开发条件等因素,本实验也存在许多可完善之处。例如合约中超时处理的逻辑可以进一步优化,合约整体可以进一步优化以节省gas,前端较为简陋,前端与合约的交互可以通过event而不一定是函数返回值。这些问题有待于进一步解决完善。

在此实验中,Microsoft Azure提供了托管前端的服务器资源、Cloudflare提供了域名解析与CDN服务、ethereum.org提供了智能合约的集成开发与测试环境、智谷提供了合约最终部署环境,在此一并向它们表示感谢。

作者

Rye Song

发布于

2024-05-09

更新于

2024-07-14

许可协议