📜 ⬆️ ⬇️

As we played the game “Stone - scissors - paper” on the Ethereum blockchain. Part 2 Technical

Taking into account the comments to my previous article, I decided to write the second part, where the technical component of the game will be considered in more detail.

So, let's begin. We made the client part in javascript using the Meteor framework nodejs. The only server solution in the game is a chat on mongoDB. See what algorithm of the game we planned before starting work:



Description of the smart contract. There are no blanks or template options for creating the game “Stone, Scissors, Paper” on the blockchain. To do this, we conducted our own research and development. Our smart contract allows you to create and close gaming tables. Information on all tables is contained in the memory of the smart contract.
')
struct room { address player1; address player2; bytes32 bit1; bytes32 bit2; uint8 res1; uint8 res2; uint256 bet; uint8 counter; uint8 c1; uint8 c2; uint roomTime; uint startTime; bool open; bool close; } mapping (uint => room) rooms; 

As you can see, we create a mapping (associative array) of rooms, the keys of which are table numbers, and the values ​​are the table object. In the table object, we declare the space for the players' addresses and their moves. Also in the table is stored the total score of the match, the number of victories on each side, and the timestamps for creating the table and starting the game. The table number is generated by the client part randomly. This number is from 0 to 99999999. The same number can be used many times, provided that the created table is closed. Pads with table numbers can occur extremely rarely, but even in this case, it does not disrupt the smart contract, the duplicate transaction simply cannot take place. If the table is not closed normally, the number used for some time will fall out of the possible options for creating the next table. According to the logic of the smart contract, an open table has a duration of relevance only knocks. After that, any action on the table leads to its closure, including an attempt to connect to it anyone. The table is closed due to the revertRoom function and then closeRoom, which removes information about the table from the associative array of rooms.

 function revertRoom(uint id) internal { if( rooms[id].bet > 0 ) { rooms[id].player1.transfer(rooms[id].bet); if( rooms[id].player2 != 0x0 ) rooms[id].player2.transfer(rooms[id].bet); } RevertRoom(id); closeRoom(id); } function closeRoom(uint id) internal { rooms[id].close = true; RoomClosed( id ); delete rooms[id]; } 

Smart contract functions that begin with a capital letter are events. The gameplay in the client part is largely based on listening to certain events related to the current table. We created 14 events:

 event RoomOpened( uint indexed room, address indexed player1, uint256 bet, uint8 counter, uint openedTime, bool indexed privat ); event RoomClosed( uint indexed room ); event JoinPlayer1( uint indexed room, address indexed player1 ); event JoinPlayer2( uint indexed room, address indexed player2, uint countdownTime ); event BetsFinished(uint indexed room ); event BetsAdd(address indexed from, uint indexed room ); event OneMoreGame(uint indexed room ); event SeedOpened( uint indexed room ); event RoundFinished( uint indexed room, uint8 res1, uint8 res2 ); event Revard(address win, uint256 amount, uint indexed room ); event Winner(address win, uint indexed room ); event Result(address indexed player, uint8 r, uint indexed room ); event RevertRoom(uint indexed room); event ScoreChanged(uint indexed room, uint8 score1, uint8 score2); 

Marking indexed after the declaration of the type of variable allows filtering events by the specified field. Listening to events on the blockchain from the client side is done as follows (We use the coffee script, it can be converted in the javascript service https://js2.coffee ):

 #  this.autorun => filter5 = contractInstance.Winner {room: Number(FlowRouter.getParam('id')), }, {fromBlock:0, toBlock: 'latest', address: contrAdress} filter5.watch (error, result) -> console.log result if result instance.winner.set result.args.win console.log result.args.win UIkit.modal("#modal-winner").show() 

As you can see, we are addressing an event on the blockchain called Winner, having previously prepared the image of a smart contract constactInstance. Setting up web3 and preparing the image is performed by the following script:

Longcode
 if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider); var contrAdress = '0x80dd7334a28579a9e96601573555db15b7fe523a'; var contrInterface = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": false, "name": "score1", "type": "uint8" }, { "indexed": false, "name": "score2", "type": "uint8" } ], "name": "ScoreChanged", "type": "event" }, { "constant": false, "inputs": [], "name": "deleteContract", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "exitRoom", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "fixResults", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "fixTimerResults", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "joinRoom", "outputs": [ { "name": "", "type": "uint256" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "count", "type": "uint8" }, { "name": "privat", "type": "bool" } ], "name": "newRoom", "outputs": [ { "name": "", "type": "uint256" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "bet", "type": "bytes32" } ], "name": "setBet", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "OneMoreGame", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "player", "type": "address" }, { "indexed": false, "name": "r", "type": "uint8" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "Result", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "SeedOpened", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "win", "type": "address" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "Winner", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": false, "name": "res1", "type": "uint8" }, { "indexed": false, "name": "res2", "type": "uint8" } ], "name": "RoundFinished", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "win", "type": "address" }, { "indexed": false, "name": "amount", "type": "uint256" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "Revard", "type": "event" }, { "constant": false, "inputs": [ { "name": "mreic", "type": "uint256" } ], "name": "setMaxReic", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "BetsFinished", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": true, "name": "player2", "type": "address" }, { "indexed": false, "name": "countdownTime", "type": "uint256" } ], "name": "JoinPlayer2", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "RevertRoom", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": true, "name": "player1", "type": "address" } ], "name": "JoinPlayer1", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "RoomClosed", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": true, "name": "player1", "type": "address" }, { "indexed": false, "name": "bet", "type": "uint256" }, { "indexed": false, "name": "counter", "type": "uint8" }, { "indexed": false, "name": "openedTime", "type": "uint256" }, { "indexed": true, "name": "privat", "type": "bool" } ], "name": "RoomOpened", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "BetsAdd", "type": "event" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "seed", "type": "uint256" } ], "name": "setSeed", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [], "name": "transferOutAll", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomBet", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomBet1", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomBet2", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomCounter", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "player", "type": "address" } ], "name": "checkRoomIsBet", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomNotClosed", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomOpened", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomPlayer1", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomPlayer2", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomRes1", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomRes2", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomScore1", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomScore2", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomStartTime", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkSenderBet", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" } ]; var contr = web3.eth.contract(contrInterface); var contractInstance = contr.at(contrAdress); var address = web3.eth.defaultAccount; var block = 0; web3.eth.getBlockNumber( function(er, res){ if(res) block = res }); } 


ContractInterface is abi smart contract - a set of function names, variables and arguments for handling and interacting with a smart contract. If you use remix - ide for working with ethereum smart contracts in the browser, it is easy to find it in the Compite tab -> Details -> Abi.

The big problem was, and still partially remains, the determination of the game state at a single point in time, when the page is reloaded or when switching between tables. Do not forget that we are not working with a server on which such a problem is solved elementarily, but with a blockchain, where to get information from is rather exotic. We constantly listen to all these 14 game events: the joining of an opponent, the beginning of moves, the completion of moves and others. We also constantly send requests for some game information, for example, a current score, the start time of the round and others. Moreover, the majority of game states are determined not by one event, and not by one variable, but by the superposition of several events with some variables at once — the results of direct data acquisition from the blockchain. For example, the situation when the game goes to two or more victories, and when the third round is the time to send private keys. We have to track that the game has begun, the opponent has connected, three rounds have passed, and the encrypted moves have been sent from both sides. Try to reload the page at this moment and you have to restore all interconnections with the blockchain. Each request or receipt of information occurs asynchronously. Delays overlap one another and as a result, the application runs significantly slower than the server version.

But back to the smart contract of our game. We have already talked about how we store game data, what events we use to interact with the client side, and how we create tables. Now I would like to describe the encryption of the moves. We have only three possible options for the move, 1 — stone, 2 — scissors, 3 — paper. It all starts on the client, when the player chooses his turn. The script, which works out the choice of the card, generates a random number - we call it seed (hereinafter - Sid). Sid is stored in cookies in relation to the table number, and is stored until further use in them and in the game session. For the first time, a seed is used when a player determines his move: choose a stone, scissors, or paper. The number 1, 2 or 3 is added to it - what the player has chosen. The result is hashed by the sha3 method. Important point: the web3.sha3 () method works only with strings. On the client side, we easily convert the resulting number to a string and then hash it to send it to a smart contract into a bit1 or bit2 variable.

 function setBet(uint id, bytes32 bet) public { require(msg.sender == rooms[id].player1 || msg.sender == rooms[id].player2 ); //   if(rooms[id].startTime + 5 minutes > now) { if(msg.sender == rooms[id].player1) { rooms[id].res2 = 5; rooms[id].bit1 = bet; } else if(msg.sender == rooms[id].player2) { rooms[id].res1 = 5; rooms[id].bit2 = bet; } if(rooms[id].bit1 != 0x0 && rooms[id].bit2 != 0x0) { SeedOpened(id); BetsFinished(id); } BetsAdd(msg.sender , id); } else { Result(rooms[id].player1, rooms[id].res1, id); Result(rooms[id].player2, rooms[id].res2, id); } } 

The setBet function provides mainly a guarantee of making moves on both sides. Until both players make their moves and they are not recorded, the game process cannot continue. At the same time, it also monitors the observance of timers and carries out the necessary initial checks on the very possibility of making a move. When the moves are done, the function sends signals to the client: SeedOpened and BetsFinisfed. Having received these signals, the client offers the players, as we say “reveal the cards”, that is, send the very first part of the number to the smart contact before summing up, which it generated randomly, before hashing the turn. Confirming his consent to the "disclosure of cards", the player sends this number to another function of the smart contract - setSeed

 function setSeed(uint256 id, uint256 seed) public { require( rooms[id].bit2 != 0x0 && rooms[id].bit1 != 0x0 ); require(msg.sender == rooms[id].player1 || msg.sender == rooms[id].player2 ); //   if(rooms[id].startTime + 5 minutes > now) { if(msg.sender == rooms[id].player1) decodeHash1(id, seed); else if(msg.sender == rooms[id].player2) decodeHash2(id, seed); } else { Result(rooms[id].player1, rooms[id].res1, id); Result(rooms[id].player2, rooms[id].res2, id); } } 

Which in turn transfers the received seed to the internal functions of the smart contract decodeHash1 and decodeHash2

 function decodeHash1(uint id, uint seed) internal { uint e1 = seed + 1; bytes32 bitHash1a = keccak256(uintToString(e1)); uint e2 = seed + 2; bytes32 bitHash1b = keccak256(uintToString(e2)); uint e3 = seed + 3; bytes32 bitHash1c = keccak256(uintToString(e3)); if(rooms[id].bit1 == bitHash1a) rooms[id].res1 = 1; if(rooms[id].bit1 == bitHash1b) rooms[id].res1 = 2; if(rooms[id].bit1 == bitHash1c) rooms[id].res1 = 3; Result(rooms[id].player1, rooms[id].res1, id); // return res1; } 

This ends the encryption cycle. The function reproduces the hashing procedure already described for the client only inside ethereum, and it does this three times for each player - in three possible moves. Then, a normal comparison of the hash result in ethereum with the existing hash result from the client occurs. Compliance makes it clear to us what move the player made. We do not declare and do not store the keys that come from the players during the disclosure; instead, we immediately write the decoding result in the variables res for each player as a number from 0 to 4.

At this stage, there is another interesting nuance. The standard function solidity keccak256, which corresponds to the sha3 method in the web3 js library, as it turned out, gives an adequate result only when the input is a string, not a number. Keccak256 allows you to work with numbers, but since on the client web3.sha3 () accepts only strings, keccak256 should also receive a string as an input. And converting numbers to strings in solidity is not as easy as using javascript. To do this, you need to write an additional internal function: uintToString (). The mark “pure” means that this function has no right to influence the state of the memory of a smart contract in any way: read and write. Here is a nuance.

 function uintToString(uint i) internal pure returns (string){ // bytes memory bstr = new bytes; if (i == 0) return "0"; uint j = i; uint length; while (j != 0){ length++; j /= 10; } bytes memory bstr = new bytes(length); uint k = length - 1; while (i != 0){ bstr[k--] = byte(48 + i % 10); i /= 10; } return string(bstr); } 

And finally, the game loop completes the winRes () internal function for determining the winner. It contains all the possible outcomes of the game, namely - when a draw begins replay of the current round. Determining the winner of a round or match. Accordingly, the concept of final and intermediate victory appears. The function understands situations where one of the players did not have time to take action and counts him a defeat in the current round. In a situation where both players have been inactive longer than they were set for, the table closes regardless of the current account. In this case, the game stops and everything returns to its original state.

Summary code
 function winRes(uint id) internal { require(rooms[id].res1 > 0 || rooms[id].res2 > 0); address win = 0x0; if(rooms[id].res1 == 1 && rooms[id].res2 == 2) win = rooms[id].player1; if(rooms[id].res1 == 1 && rooms[id].res2 == 3) win = rooms[id].player2; if(rooms[id].res1 == 2 && rooms[id].res2 == 1) win = rooms[id].player2; if(rooms[id].res1 == 2 && rooms[id].res2 == 3) win = rooms[id].player1; if(rooms[id].res1 == 3 && rooms[id].res2 == 1) win = rooms[id].player1; if(rooms[id].res1 == 3 && rooms[id].res2 == 2) win = rooms[id].player2; if(rooms[id].res1 == 4 && rooms[id].res2 != 4 ) win = rooms[id].player2; if(rooms[id].res2 == 4 && rooms[id].res1 != 4 ) win = rooms[id].player1; if(rooms[id].res1 == 5 && rooms[id].res2 != 5 ) win = rooms[id].player2; if(rooms[id].res2 == 5 && rooms[id].res1 != 5 ) win = rooms[id].player1; if((rooms[id].res2 == 4 && rooms[id].res1 == 4 ) || (rooms[id].res2 == 5 && rooms[id].res1 == 5 )) revertRoom(id); else { // -    if(win == 0x0) { replay(id); OneMoreGame(id); } else { //   if( win == rooms[id].player1 ) rooms[id].c1 += 1; if( win == rooms[id].player2 ) rooms[id].c2 += 1; //    n-      if( rooms[id].counter > 1 && rooms[id].c1 < rooms[id].counter && rooms[id].c2 < rooms[id].counter ) { ScoreChanged(id, rooms[id].c1, rooms[id].c2); replay(id); OneMoreGame(id); } else { //          ScoreChanged(id, rooms[id].c1, rooms[id].c2); if( rooms[id].bet > 0 ) { rewardWin(win, id); } Winner(win, id); closeRoom(id); } } } } 


The game's algorithm was created from scratch, so this option is a successful result of a series of experiments and adjustments. It is difficult to say how effective and optimal it is, because we can only compare it with ourselves. Regarding the previous 4 versions of our algorithm, this one is several times more efficient. There are probably many more possible solutions for optimization, but this is already a matter of the future.

Source: https://habr.com/ru/post/358772/


All Articles