I made a provably fair 1v1 coin flip game in Laravel and I want to be sure it's safe (cannot be hacked) and that the outcome is random (50/50).
At first Player 1 starts the bet and the hash will be created. The hash is a SHA256 of the $secret
, which will be revealed to the players after the game. This hash ensures that the game outcome depends on the client and not on the server (provably fair). Does it matter which hash type I use? Are they all equally safe?
$secret = str_random(40);
$bet = new Bet;
$bet->user_id = Auth::id();
$bet->amount = $amount;
$bet->secret_key = $secret;
$bet->secret_hash = hash('sha256', $secret);
$bet->save();
So after that, player 2 will be able to join player 1's bet. When player 2 accepts the bet, this is what happens:
$(document).delegate('.flipcoin-btn', 'click', function() {
// When player 2 clicks the button
SubmitFlipCoin($(this).attr('id'), CreateClientKey());
});
function CreateClientKey()
{
var outStr = "", newStr;
while(outStr.length < 20)
{
newStr = Math.random().toString(36).slice(2);
outStr += newStr.slice(0, Math.min(newStr.length, (20 - outStr.length)));
}
console.log("Client string created: "+outStr.toUpperCase());
return outStr.toUpperCase();
};
function SubmitFlipCoin(id, client)
{
StopMoneyUpdate = 1;
$.ajax({
method: 'POST',
url: flipcoin_link,
data: {id: id, clientstr: client, _token: token},
dataType: 'JSON',
})
.done(function(result) {
if(result.hasOwnProperty("error")) {
ShowBetError('<b>'+result.error+'</b>');
}
})
.fail(function(error) {
ShowBetError('<b>An error occurred while flipping the coin, please try again.</b>');
});
}
As you can see, the client (player 2) generates a random string, which will be send to the server. The server will then make an HMAC hash of bet_id+clientstring
with the $secret
as key. The outcome of the game is determined by this HMAC hash. It takes the second number of the HMAC hash. If the result is 0 to 4 then player 1 wins, and if result is 5 to 9 player 2 will win.
$resulthash = hash_hmac('sha512', $request->clientstr."-".$bet->id, $bet->secret_key);
$resultnumber = filter_var($resulthash, FILTER_SANITIZE_NUMBER_INT);
if($resultnumber[1] < 5) {
$result = 0;
}
else {
$result = 1;
}
EDIT: I did some testing to see if the outcome is really 50/50 and it seems pretty accurate.
$dealer = 0;
$player = 0;
$times = 1000000;
for($i = 0; $i < $times; $i++) {
$result = filter_var(hash('sha512', rand()), FILTER_SANITIZE_NUMBER_INT)[1];
if($result < 5) {
$dealer++;
}
else {
$player++;
}
}
echo "Dealer: ".$dealer/$times * 100;
echo "</p>Player: ".$player/$times * 100;
This was the outcome that I got:
Dealer: 49.9891 Player: 50.0109
random_int()
\$\endgroup\$