通过实例学习,建立一个安全的高赌注随机数游戏
- 原文:https://soliditydeveloper.com/high-stakes-roulette
- 译文出自:登链翻译计划
- 译者:翻译小组
- 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
在上一篇,我们介绍了区块链与随机数,介绍使用承诺模式来安全在智能合约中生成一个随机数。
举一反三的学习总是最好的。 所以,让我们在区块链上建立一个真实的随机数游戏。 我们将通过增加一个安全的随机数生成器,使其足够安全,允许游戏支持真正的高赌注。
先来讨论一下整体设计。
作为开发人员,在进行任何编程之前,我们要正确规划和设计我们的系统。 在随机数游戏中,我们需要弄清楚如何创建一个随机数,如何管理资金以及如何处理超时。
合约的核心将是一个承诺模式。在这里可以查看之前的教程。 但总的来说,区块链中的随机数没有很好的直接来源,因为所有的代码都要确定性地运行。
低赌注的方案: 使用未来的区块哈希是一个可能的解决方案,但矿工对这个值有一定的影响。 他们可以选择不发布新区块,放弃区块奖励。 但如果他们同时在玩一个非常高赌注的随机数游戏,阻止一个区块可能是他们更好的收益策略。
高额赌注方案: 所以对于高赌注的情况,我们需要一个更好的随机数发生器。 幸运的是,在有两个参与者(在本文中,我们设定两个玩家为:银行和玩家)的设置中,我们可以使用承诺模式。 每个玩家承诺秘密随机数,首先发送该数字的keccak256承诺哈希值。 一旦两个哈希值都在合约中,玩家就可以安全地揭示实际的随机数。 该合约验证了keccak256(randomNumber)==承诺哈希
,确保双方不能再更改随机数。 最后的随机数将是这个随机数的运算(如:randomNumberBank XOR randomNumberPlayer)。 更多的细节在可参看区块链与随机数。
针对这种高额赌注的情况,可以从两个方面进行改进:
利用这两项改进,单场比赛的回合数就可以减少到:
我们不想为每一轮游戏发资金。 所以就像在现实世界中一样,我们的赌场会管理自己的资金。 玩家和银行在合约中存入资金,并获得游戏内资金。 他们可以随时提取任何解锁资金或存入更多资金。
在承诺模式中,有一种方法可以操纵结果:不发送秘密数值,从而阻止一个回合的结束。 为了处理这种情况,我们需要为玩家提供一个额外的功能,检查银行是否在规定的时间内发送了秘密数值。如果没有发送,则玩家自动获胜。
可以在存储中声明一个简单的映射。
mapping (address => uint256) public registeredFunds;
在这里,我们可以在发送ETH时记录地址存入的金额,或者在取出ETH时减去提取的金额。 你同样可以用ERC-20代币代替ETH。
我们使用.call
方法而不是.transfer
,因为transfer是不推荐发送ETH的方式了。
function depositFunds() external payable {
require(msg.value > 0, "Must send ETH");
registeredFunds[msg.sender] += msg.value;
}
function withdrawFunds() external {
require(registeredFunds[msg.sender] > 0);
uint256 funds = registeredFunds[msg.sender];
registeredFunds[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: funds}("");
require(success, "ETH transfer failed");
}
接下来让我们来创建实际的游戏。 一轮比赛将由GameRound结构来定义。 下面几个值将用来生成随机数:
userValue作为选择是赌红还是赌黑(更好的编码方式可能是在这里使用一个枚举,有两个值RED和BLACK)。 将赢取的资金作为lockedFunds。 对于红/黑的投注,lockedFunds将是投注额的两倍。 并且我们还需要存储下注的时间,以实现超时功能。
struct GameRound {
bytes32 bankHash;
uint256 bankSecretValue;
uint256 userValue;
bool hasUserBetOnRed;
uint256 timeWhenSecretUserValueSubmitted;
uint256 lockedFunds;
}
现在我们可以创建一个placeBet
函数。 确保游戏处于正确的状态,并确保银行和玩家有足够的资金。 我们会存储赌注,锁定资金,存储超时时间。
为什么我们为每个玩家存储一个银行哈希值?你可能会好奇为什么我们不在所有游戏中只使用一个银行哈希值。 这似乎很诱人,因为它减少了银行的复杂性。 不幸的是,它将允许完全操纵随机数。 想象一下,多个玩家同时下注。 现在,银行可以决定为哪位玩家揭示秘密数值。 为了防止这种情况的发生,我们需要根据投注的时间,对每一次的揭晓执行严格的顺序。 这最终会比每个玩家有一个哈希更复杂。
function placeBet(bool hasUserBetOnRed, uint256 userValue,uint256 _betAmount) external {
require(gameRounds[msg.sender].bankHash != 0x0, "Bank hash not yet set");
require(userValue == 0, "Already placed bet");
require(registeredFunds[bankAddress] >= _betAmount, "Not enough bank funds");
require(registeredFunds[msg.sender] >= _betAmount, "Not enough user funds");
gameRounds[msg.sender].userValue = userValue;
gameRounds[msg.sender].hasUserBetOnRed = hasUserBetOnRed;
gameRounds[msg.sender].lockedFunds = _betAmount * 2;
gameRounds[userAddress].timeWhenSecretUserValueSubmitted = block.timestamp;
registeredFunds[msg.sender] -= _betAmount;
registeredFunds[bankAddress] -= _betAmount;
}
你可能已经注意到,在第一轮之前,bankhash 会是空的。 所以我们需要两个额外的函数,这些函数只在开始时被玩家调用一次。 通过initializeGame
玩家可以请求银行调用setInitialBankHash
。
function initializeGame() external {
require(!hasRequestedGame[msg.sender],"Already requested");
hasRequestedGame[msg.sender] = true;
emit NewGameRequest(msg.sender);
}
银行将运行一个服务器,监听NewGameRequest
事件。 收到事件后,将调用 setInitialBankHash。
function setInitialBankHash(
bytes32 bankHash,
address user
) external onlyOwner {
require(
gameRounds[user].bankHash == 0x0,
"Bank hash already set"
);
gameRounds[user].bankHash = bankHash;
}
现在进行实际游戏,银行需要揭示数值。我们要求游戏回合确实是在(等待)银行揭示数值状态下。 同时我们确保hashReveal等于gameRounds[userAddress].bankHash,因此强制要求银行不能操纵随机数。
function sendBankSecretValue(uint256 bankSecretValue, address user) external {
require(gameRounds[userAddress].userValue != 0, "User has no value set");
require(gameRounds[userAddress].bankSecretValue == 0, "Already revealed");
bytes32 hashedReveal = keccak256(abi.encodePacked(bankSecretValue));
require(hashedReveal == gameRounds[userAddress].bankHash, "Bank reveal not matching commitment");
gameRounds[userAddress].bankSecretValue = bankSecretValue;
_evaluateBet(user);
_resetContractFor(user);
gameRounds[userAddress].bankHash = bytes32(bankSecretValue);
}
然后我们确定结果,看看谁赢了。 最后我们重新设置下一轮的数据,其中包括自动将银行哈希值设置为当前的秘密数值(根据我们在开始时描述的承诺哈希链设计)。
function _resetContractFor(address user) private {
gameRounds[user] = GameRound(0x0, 0, 0, false, 0, 0);
}
function _evaluateBet(address user) private {
uint256 random = gameRounds[user].bankSecretValue
^ gameRounds[user].userValue;
uint256 number = random % ROULETTE_NUMBER_COUNT;
uint256 winningAmount = gameRounds[user].lockedFunds;
bool isNeitherRedNorBlack = number == 0;
bool isRed = isNumberRed[number];
bool hasUserBetOnRed = gameRounds[user].hasUserBetOnRed;
address winner;
if (isNeitherRedNorBlack) winner = bankAddress;
else if (isRed == hasUserBetOnRed) winner = userAddress;
else winner = bankAddress;
registeredFunds[winner] += winningAmount;
}
我们现在有两个由玩家和银行随机选择的号码。 使用按位 OR 可以计算出一个最终的随机数。
使用 随机数 % ROULETTE_NUMBER_COUNT
,例如,计算随机数模37,将得到一个0到36之间的随机数,获得任何数字都有相同的概率。
现在对于选出优胜者,我们有三个情况:
为了确定颜色是否为红色,我们可以使用存储bool[37] isNumberRed
数组来定义。
确定预先定义的超时时间TIMEOUT_FOR_BANK_REVEAL
(例如2天),我们可以检查是否有超时。 如果游戏确实是在等待银行发送揭示,并且等待的时间超过了超时时间,玩家可以调用checkBankSecretValueTimeout
,将自动赢得游戏回合。
function checkBankSecretValueTimeout() external {
require(gameRounds[msg.sender].bankHash != 0, "Bank hash not set");
require(gameRounds[msg.sender].bankSecretValue == 0, "Bank secret is set");
require(gameRounds[msg.sender].userValue != 0, "User value not set");
uint256 timeout = (gameRounds[msg.sender].timeWhenSecretUserValueSubmitted + TIMEOUT_FOR_BANK_REVEAL);
require(block.timestamp > timeout, "Timeout not yet reached");
registeredFunds[msg.sender] += gameRounds[msg.sender].lockedFunds;
_resetContractFor(msg.sender);
hasRequestedGame[msg.sender] = false;
}
完整可行的代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BankOwned {
address public bankAddress;
constructor() {
bankAddress = msg.sender;
}
modifier onlyOwner {
require(msg.sender == bankAddress);
_;
}
}
contract Roulette is BankOwned {
uint256 public immutable TIMEOUT_FOR_BANK_REVEAL = 1 days;
uint256 public immutable ROULETTE_NUMBER_COUNT = 37;
// prettier-ignore
bool[37] isNumberRed = [false, true, false, true, false, true, false, true, false, true, false, false, true, false, true, false, true, false, true, true, false, true, false, true, false, true, false, true, false, false, true, false, true, false, true, false, true];
struct GameRound {
bytes32 bankHash;
uint256 bankSecretValue;
uint256 userValue;
bool hasUserBetOnRed;
uint256 timeWhenSecretUserValueSubmitted;
uint256 lockedFunds;
}
mapping(address => bool) public hasRequestedGame;
mapping(address => GameRound) public gameRounds;
mapping(address => uint256) public registeredFunds;
event NewGameRequest(address indexed user);
function increaseFunds() external payable {
require(msg.value > 0, "Must send ETH");
registeredFunds[msg.sender] += msg.value;
}
function withdrawMoney() external {
require(registeredFunds[msg.sender] > 0);
uint256 funds = registeredFunds[msg.sender];
registeredFunds[msg.sender] = 0;
(bool wasSuccessful, ) = msg.sender.call{value: funds}("");
require(wasSuccessful, "ETH transfer failed");
}
function initializeGame() external {
require(!hasRequestedGame[msg.sender], "Already requested game");
hasRequestedGame[msg.sender] = true;
emit NewGameRequest(msg.sender);
}
function setInitialBankHash(bytes32 bankHash, address userAddress) external onlyOwner {
require(gameRounds[userAddress].bankHash == 0x0, "Bank hash already set");
gameRounds[userAddress].bankHash = bankHash;
}
function placeBet(
bool hasUserBetOnRed,
uint256 userValue,
uint256 _betAmount
) external {
require(gameRounds[msg.sender].bankHash != 0x0, "Bank hash not yet set");
require(userValue == 0, "Already placed bet");
require(registeredFunds[bankAddress] >= _betAmount, "Not enough bank funds");
require(registeredFunds[msg.sender] >= _betAmount, "Not enough user funds");
gameRounds[msg.sender].userValue = userValue;
gameRounds[msg.sender].hasUserBetOnRed = hasUserBetOnRed;
gameRounds[msg.sender].lockedFunds = _betAmount * 2;
gameRounds[userAddress].timeWhenSecretUserValueSubmitted = block.timestamp;
registeredFunds[msg.sender] -= _betAmount;
registeredFunds[bankAddress] -= _betAmount;
}
function sendBankSecretValue(uint256 bankSecretValue, address userAddress) external {
require(gameRounds[userAddress].userValue != 0, "User has no value set");
require(gameRounds[userAddress].bankSecretValue == 0, "Already revealed");
require(keccak256(abi.encodePacked(bankSecretValue)) == gameRounds[userAddress].bankHash, "Bank reveal not matching commitment");
gameRounds[userAddress].bankSecretValue = bankSecretValue;
_evaluateBet(userAddress);
_resetContractFor(userAddress);
gameRounds[userAddress].bankHash = bytes32(bankSecretValue);
}
function checkBankSecretValueTimeout() external {
require(gameRounds[msg.sender].bankHash != 0, "Bank hash not set");
require(gameRounds[msg.sender].bankSecretValue == 0, "Bank secret is set");
require(gameRounds[msg.sender].userValue != 0, "User value not set");
require(block.timestamp > (gameRounds[msg.sender].timeWhenSecretUserValueSubmitted + TIMEOUT_FOR_BANK_REVEAL), "Timeout not yet reached");
registeredFunds[msg.sender] += gameRounds[msg.sender].lockedFunds;
_resetContractFor(msg.sender);
}
function _resetContractFor(address userAddress) private {
gameRounds[userAddress] = GameRound(0x0, 0, 0, false, 0, 0);
}
function _evaluateBet(address userAddress) private {
uint256 random = gameRounds[userAddress].bankSecretValue ^ gameRounds[userAddress].userValue;
uint256 number = random % ROULETTE_NUMBER_COUNT;
uint256 winningAmount = gameRounds[userAddress].lockedFunds;
bool isNeitherRedNorBlack = number == 0;
bool isRed = isNumberRed[number];
bool hasUserBetOnRed = gameRounds[userAddress].hasUserBetOnRed;
address winner;
if (isNeitherRedNorBlack) winner = bankAddress;
else if (isRed == hasUserBetOnRed) winner = userAddress;
else winner = bankAddress;
registeredFunds[winner] += winningAmount;
}
}
此代码也可以在这里找到。 不过你要知道,这只是合约代码。 作为银行提供者,你需要一个后台服务器运行,处理监听新投注和发送承诺哈希的逻辑。 通过允许银行同时为多个玩家提交多个哈希,可以进一步改进 gas 消耗。
另外,一个漂亮的前端界面也肯定受玩家们欢迎。
本翻译由 Cell Network 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!