如何设计以太坊上的高额赌注随机数游戏
通过实例学习,建立一个安全的高赌注随机数游戏
> * 原文:https://soliditydeveloper.com/high-stakes-roulette > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/2154) 在上一篇,我们介绍了[区块链与随机数](https://learnblockchain.cn/article/2148),介绍使用承诺模式来安全在智能合约中生成一个随机数。 举一反三的学习总是最好的。 所以,让我们在区块链上建立一个真实的随机数游戏。 我们将通过增加一个安全的随机数生成器,使其足够安全,允许游戏支持真正的高赌注。 先来讨论一下整体设计。 ## 设计合约 作为开发人员,在进行任何编程之前,我们要正确规划和设计我们的系统。 在随机数游戏中,我们需要弄清楚如何创建一个随机数,如何管理资金以及如何处理超时。 ### 安全地生成一个随机数 合约的核心将是一个承诺模式。在[这里](https://learnblockchain.cn/article/2148)可以查看之前的教程。 但总的来说,区块链中的随机数没有很好的直接来源,因为所有的代码都要确定性地运行。 **低赌注的方案:** 使用未来的区块哈希是一个可能的解决方案,但矿工对这个值有一定的影响。 他们可以选择不发布新区块,放弃区块奖励。 但如果他们同时在玩一个非常高赌注的随机数游戏,阻止一个区块可能是他们更好的收益策略。 **高额赌注方案:** 所以对于高赌注的情况,我们需要一个更好的随机数发生器。 幸运的是,在有两个参与者(在本文中,我们设定两个玩家为:**银行和玩家**)的设置中,我们可以使用[承诺模式](https://en.wikipedia.org/wiki/Commitment_scheme)。 每个玩家承诺秘密随机数,首先发送该数字的keccak256承诺哈希值。 一旦两个哈希值都在合约中,玩家就可以安全地揭示实际的随机数。 该合约验证了`keccak256(randomNumber)==承诺哈希`,确保双方不能再更改随机数。 最后的随机数将是这个随机数的运算(如:randomNumberBank [XOR](https://en.wikipedia.org/wiki/XOR_gate) randomNumberPlayer)。 更多的细节在可参看[区块链与随机数](https://learnblockchain.cn/article/2148)。 ![随机数](https://img.learnblockchain.cn/pics/20210215085627.png) #### 完善高额赌注下的设计 针对这种高额赌注的情况,可以从两个方面进行改进: 1. 让银行先承诺一个秘密数值发送哈希,其实就足够了。 当银行提交后,玩家可以直接揭示自己的数值。 这样就减少了一个游戏回合,避免了玩家也发送承诺哈希。 2. 假设一个玩家不会只玩一个回合,我们可以建立一个承诺哈希链。 该链的计算方式为:keccak256(keccak256(keccak256( ... (randomNumber) ...))。 基本上是重复的计算出哈希的哈希值,以此类推,数百万次。最后一个哈希值将发送作为第一个承诺,而所有其他中间哈希值则存储起来供以后使用。 然后,银行揭示的秘密值又将自动作为下一轮的承诺。 利用这两项改进,单场比赛的回合数就可以减少到: 1. 玩家在下注的同时发送一个秘密数值。 为了简单起见,我们只允许投注红色或黑色,但应该很容易扩展该功能。 2. 银行发送秘密数值时(揭示阶段),将自动触发支付。 ## 基金管理 我们不想为每一轮游戏发资金。 所以就像在现实世界中一样,我们的赌场会管理自己的资金。 玩家和银行在合约中存入资金,并获得游戏内资金。 他们可以随时提取任何解锁资金或存入更多资金。 ### 处理超时 在承诺模式中,有一种方法可以操纵结果:不发送秘密数值,从而阻止一个回合的结束。 为了处理这种情况,我们需要为玩家提供一个额外的功能,检查银行是否在规定的时间内发送了秘密数值。如果没有发送,则玩家自动获胜。 ## 管理合约中的资金 可以在存储中声明一个简单的映射。 ```javascript mapping (address => uint256) public registeredFunds; ``` 在这里,我们可以在发送ETH时记录地址存入的金额,或者在取出ETH时减去提取的金额。 你同样可以用ERC-20代币代替ETH。 我们使用`.call`方法而不是`.transfer`,因为transfer是[不推荐](https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/)发送ETH的方式了。 ```javascript 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结构来定义。 下面几个值将用来生成随机数: - bankHash - bankSecretValue - userValue userValue作为选择是赌红还是赌黑(更好的编码方式可能是在这里使用一个枚举,有两个值RED和BLACK)。 将赢取的资金作为lockedFunds。 对于红/黑的投注,lockedFunds将是投注额的两倍。 并且我们还需要存储下注的时间,以实现超时功能。 ```javascript struct GameRound { bytes32 bankHash; uint256 bankSecretValue; uint256 userValue; bool hasUserBetOnRed; uint256 timeWhenSecretUserValueSubmitted; uint256 lockedFunds; } ``` 现在我们可以创建一个`placeBet`函数。 确保游戏处于正确的状态,并确保银行和玩家有足够的资金。 我们会存储赌注,锁定资金,存储超时时间。 **为什么我们为每个玩家存储一个银行哈希值?**你可能会好奇为什么我们不在所有游戏中只使用一个银行哈希值。 这似乎很诱人,因为它减少了银行的复杂性。 不幸的是,它将允许完全操纵随机数。 想象一下,多个玩家同时下注。 现在,银行可以决定为哪位玩家揭示秘密数值。 为了防止这种情况的发生,我们需要根据投注的时间,对每一次的揭晓执行严格的顺序。 这最终会比每个玩家有一个哈希更复杂。 ![随机42](https://img.learnblockchain.cn/pics/20210215085637.jpg) ```javascript 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`。 ```javascript function initializeGame() external { require(!hasRequestedGame[msg.sender],"Already requested"); hasRequestedGame[msg.sender] = true; emit NewGameRequest(msg.sender); } ``` 银行将运行一个服务器,监听`NewGameRequest`事件。 收到事件后,将调用 setInitialBankHash。 ```javascript 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,因此强制要求银行不能操纵随机数。 ```javascript 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); } ``` ## 确定结果 然后我们确定结果,看看谁赢了。 最后我们重新设置下一轮的数据,其中包括自动将银行哈希值设置为当前的秘密数值(根据我们在开始时描述的承诺哈希链设计)。 ```javascript 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之间的随机数,获得任何数字都有相同的概率。 现在对于选出优胜者,我们有三个情况: 1. 数字是0: 银行赢 2. 颜色被玩家猜对了 3. 颜色没有被玩家猜对 为了确定颜色是否为红色,我们可以使用存储`bool[37] isNumberRed`数组来定义。 ## 处理超时 确定预先定义的超时时间`TIMEOUT_FOR_BANK_REVEAL`(例如2天),我们可以检查是否有超时。 如果游戏确实是在等待银行发送揭示,并且等待的时间超过了超时时间,玩家可以调用`checkBankSecretValueTimeout`,将自动赢得游戏回合。 ```javascript 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; } ``` ## 完全可行的例子 + 缺少的组件 完整可行的代码如下: ```javascript // 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; } } ``` 此代码也可以在[这里](https://gist.github.com/gorgos/2b161ddb508989f4f0d733602054831f)找到。 不过你要知道,这只是合约代码。 作为银行提供者,你需要一个后台服务器运行,处理监听新投注和发送承诺哈希的逻辑。 通过允许银行同时为多个玩家提交多个哈希,可以进一步改进 gas 消耗。 另外,一个漂亮的前端界面也肯定受玩家们欢迎。 ------ 本翻译由 [Cell Network](https://www.cellnetwork.io/?utm_souce=learnblockchain) 赞助支持。
- 原文:https://soliditydeveloper.com/high-stakes-roulette
- 译文出自:登链翻译计划
- 译者:翻译小组
- 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
在上一篇,我们介绍了区块链与随机数,介绍使用承诺模式来安全在智能合约中生成一个随机数。
举一反三的学习总是最好的。 所以,让我们在区块链上建立一个真实的随机数游戏。 我们将通过增加一个安全的随机数生成器,使其足够安全,允许游戏支持真正的高赌注。
先来讨论一下整体设计。
设计合约
作为开发人员,在进行任何编程之前,我们要正确规划和设计我们的系统。 在随机数游戏中,我们需要弄清楚如何创建一个随机数,如何管理资金以及如何处理超时。
安全地生成一个随机数
合约的核心将是一个承诺模式。在这里可以查看之前的教程。 但总的来说,区块链中的随机数没有很好的直接来源,因为所有的代码都要确定性地运行。
低赌注的方案: 使用未来的区块哈希是一个可能的解决方案,但矿工对这个值有一定的影响。 他们可以选择不发布新区块,放弃区块奖励。 但如果他们同时在玩一个非常高赌注的随机数游戏,阻止一个区块可能是他们更好的收益策略。
高额赌注方案: 所以对于高赌注的情况,我们需要一个更好的随机数发生器。 幸运的是,在有两个参与者(在本文中,我们设定两个玩家为:银行和玩家)的设置中,我们可以使用承诺模式。 每个玩家承诺秘密随机数,首先发送该数字的keccak256承诺哈希值。 一旦两个哈希值都在合约中,玩家就可以安全地揭示实际的随机数。 该合约验证了keccak256(randomNumber)==承诺哈希
,确保双方不能再更改随机数。 最后的随机数将是这个随机数的运算(如:randomNumberBank XOR randomNumberPlayer)。 更多的细节在可参看区块链与随机数。
完善高额赌注下的设计
针对这种高额赌注的情况,可以从两个方面进行改进:
- 让银行先承诺一个秘密数值发送哈希,其实就足够了。 当银行提交后,玩家可以直接揭示自己的数值。 这样就减少了一个游戏回合,避免了玩家也发送承诺哈希。
- 假设一个玩家不会只玩一个回合,我们可以建立一个承诺哈希链。 该链的计算方式为:keccak256(keccak256(keccak256( ... (randomNumber) ...))。 基本上是重复的计算出哈希的哈希值,以此类推,数百万次。最后一个哈希值将发送作为第一个承诺,而所有其他中间哈希值则存储起来供以后使用。 然后,银行揭示的秘密值又将自动作为下一轮的承诺。
利用这两项改进,单场比赛的回合数就可以减少到:
- 玩家在下注的同时发送一个秘密数值。 为了简单起见,我们只允许投注红色或黑色,但应该很容易扩展该功能。
- 银行发送秘密数值时(揭示阶段),将自动触发支付。
基金管理
我们不想为每一轮游戏发资金。 所以就像在现实世界中一样,我们的赌场会管理自己的资金。 玩家和银行在合约中存入资金,并获得游戏内资金。 他们可以随时提取任何解锁资金或存入更多资金。
处理超时
在承诺模式中,有一种方法可以操纵结果:不发送秘密数值,从而阻止一个回合的结束。 为了处理这种情况,我们需要为玩家提供一个额外的功能,检查银行是否在规定的时间内发送了秘密数值。如果没有发送,则玩家自动获胜。
管理合约中的资金
可以在存储中声明一个简单的映射。
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结构来定义。 下面几个值将用来生成随机数:
- bankHash
- bankSecretValue
- userValue
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之间的随机数,获得任何数字都有相同的概率。
现在对于选出优胜者,我们有三个情况:
- 数字是0: 银行赢
- 颜色被玩家猜对了
- 颜色没有被玩家猜对
为了确定颜色是否为红色,我们可以使用存储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 赞助支持。
- 发表于 2021-02-19 16:16
- 阅读 ( 3676 )
- 学分 ( 107 )
- 分类:以太坊
评论