There is a problem - it is difficult to generate a random number in a decentralized network. Almost all blockchains have already encountered this. Indeed, in networks where there is no trust between users, the creation of an undeniable random number solves many problems.
The article tells how to solve the problem using games as an example. The first of these was the
Waves Xmas Tree . For development, we needed a random number generator.
Initially, we planned to generate a number based on information from the blockchain. However, then it became clear: the number could be rigged, which means the solution does not fit.
We came up with a workaround: use the commit-disclosure scheme. The server “guessed” a number from 1 to 5, added “salt” to it, and then hashed the result using
the Keccak function . The server pre-deployed a smart contract with an already saved number. It turns out that the game was reduced to the fact that the user guessed the number hidden by the hash.
The player made a bet, and the server sent the requested number and "salt" to the smart contract. In plain language, he revealed the cards. After that, the server verified the numbers and decided whether the user won or lost.
If the server did not send a number or “salt” for verification, the user won. In this case, for each game it was necessary to deploy a smart contract in advance and lay potential winnings in it. It turned out to be inconvenient, long and expensive. At that time, there was no other safe solution.
Recently, the Tradisys team has proposed adding the
rsaVerify () function to the Waves protocol. It verifies the validity of the RSA signature based on the public and private key. As a result, the function was added.
We developed three games:
Dice Roller ,
Coin Flip and
Ride On Waves . Each implemented random number technology. Let's see how it works.
Consider random number generation using Ride on Waves as an example. A smart contract can be found
here .
Go to the
Script tab and select
Decompiled . See the code of the smart contract (aka script).
The smart contract code contains a set of functions. Those marked as @Callable can be triggered using
Invocation transactions . We are interested in two functions:
bet and
withdraw :
- func bet (playerChoice)
- func withdraw (gameId, rsaSign)
1. The user selects the length of the segment and the size of the bet.
2. The client forms a bet-function. For the image above, this will be
bet (“50”) .
3. The client sends the Invocation transaction to the address of the smart contract (broadcast InvocationTx). A transaction as a Call parameter contains the bet function. This means that the Invocation transaction starts the execution of the bet function (choice: String) on the smart contract.
4. Consider the bet function:
@Callable(i) func bet (playerChoice) = { let newGameNum = IncrementGameNum() let gameId = toBase58String(i.transactionId) let pmt = extract(i.payment) let betNotInWaves = isDefined(pmt.assetId) let feeNotInWaves = isDefined(pmt.assetId) let winAmt = ValidateBetAndDefineWinAmt(pmt.amount, playerChoice) let txIdUsed = isDefined(getString(this, gameId)) if (betNotInWaves) then throw ("Bet amount must be in Waves") else if (feeNotInWaves) then throw ("Transaction's fee must be in Waves") else if (txIdUsed) then throw ("Passed txId had been used before. Game aborted.") else { let playerPubKey58 = toBase58String(i.callerPublicKey) let gameDataStr = FormatGameDataStr(STATESUBMITTED, playerChoice, playerPubKey58, height, winAmt, "") ScriptResult(WriteSet(cons(DataEntry(RESERVATIONKEY, ValidateAndIncreaseReservedAmt(winAmt)), cons(DataEntry(GAMESCOUNTERKEY, newGameNum), cons(DataEntry(gameId, gameDataStr), nil)))), TransferSet(cons(ScriptTransfer(SERVER, COMMISSION, unit), nil))) } }
The function writes a new game to the state of the smart contract. Namely:
- Unique identifier for a new game (game id)
- Game state = SUBMITTED
- Player selection (line length 50)
- Public key
- Potential winnings (depending on the player’s bet)
This is how the data record in the blockchain looks like (key-value):
{ "type": "string", "value": "03WON_0283_448t8Jn9P3717UnXFEVD5VWjfeGE5gBNeWg58H2aJeQEgJ_06574069_09116020000_0229", "key": "2GKTX6NLTgUrE4iy9HtpSSHpZ3G8W4cMfdjyvvnc21dx" }
"Key" (key) -
game id of the new game. The remaining data is contained in the line of the field “value” (value). These entries are stored in the
Data tab of the smart contract:
5. The server “looks” at the smart contract and finds the sent transaction (new game) using the blockchain Api. The game id of the new game is already recorded on the blockchain, which means that it is no longer possible to change or influence it
6. The server forms a withdraw function (gameId, rsaSign). For example, this:
withdraw ("FwsuaaShC6DMWdSWQ5osGWtYkVbTEZrsnxqDbVx5oUpq", "base64:Gy69dKdmXUEsAmUrpoWxDLTQOGj5/qO8COA+QjyPVYTAjxXYvEESJbSiCSBRRCOAliqCWwaS161nWqoTL/TltiIvw3nKyd4RJIBNSIgEWGM1tEtNwwnRwSVHs7ToNfZ2Dvk/GgPUqLFDSjnRQpTHdHUPj9mQ8erWw0r6cJXrzfcagKg3yY/0wJ6AyIrflR35mUCK4cO7KumdvC9Mx0hr/ojlHhN732nuG8ps4CUlRw3CkNjNIajBUlyKQwpBKmmiy3yJa/QM5PLxqdppmfFS9y0sxgSlfLOgZ51xRDYuS8NViOA7c1JssH48ZtDbBT5yqzRJXs3RnmZcMDr/q0x6Bg==")
7. The server sends the Invocation transaction to the smart contract (broadcast InvocationTx). The transaction contains a call to the generated withdraw function (gameId, rsaSign):
The function contains the
game id of the new game and the result of the RSA signature of the unique identifier with a private key. The result of the signature is unchanged.
What does it mean?
We take the same value (game id) and apply the RSA signature method to it. We will always get the same result. This is how the RSA algorithm works. You cannot manipulate the final number, since the game id and the result of using RSA are not known. Matching a number is also pointless.
8. The blockchain accepts the transaction. It runs the withdraw function (gameId, rsaSign)
9. Inside the withdraw function, the
GenerateRandInt function (gameId, rsaSign) is exported. This is a random number generator
# @return 1 ... 100 func GenerateRandInt (gameId,rsaSign) = { # verify RSA signature to proof random let rsaSigValid = rsaVerify (SHA256, toBytes(gameId), rsaSign, RSAPUBLIC) if (rsaSigValid) then { let rand = (toInt(sha256(rsaSign)) % 100) if ((0 > rand)) then ((-1 * rand) + 1) else (rand + 1) } else throw ("Invalid RSA signature") }
rand - and there is a random number.
First, a string is taken, which is the result of the RSA signature of
game id with the private key (
rsaSign ). It is then hashed using SHA-256 (
sha256 (rsaSign) ).
We cannot predict the result of the signature and subsequent hashing. Therefore, it is impossible to influence the generation of a random number. To get a number in a certain range (for example, from 1 to 100), the toInt and% 100 conversion function is used (analogue to
mod ).
At the beginning of the article, we mentioned the
rsaVerify () function, which allows you to check the validity of RSA signatures with a private key using a public key. Here is the GenerateRandInt (gameId, rsaSign) part:
rsaVerify (SHA256, toBytes(gameId), rsaSign, RSAPUBLIC)
The RSAPUBLIC public key and the rsaSign string are passed to the input. Signature is checked for validity. A number is generated if the check is successful. Otherwise, the system considers that the signature is not valid (Invalid RSA signature).
The server must sign the game id with the private key and send a valid Rsa signature within 2880 blocks. The parameter is configured when deploying a smart contract. If nothing happens during the allotted time, the user wins. In this case, the prize must be sent to your address yourself. It turns out that the server is "not profitable to cheat," because this leads to a loss. Below is an example.
User plays
Dice Roller . I chose 2 out of 6 faces of the cube, the bet is 14 WAVES. If the server does not send a valid RSA signature on the smart contract within the set time (2880 blocks), the user will take 34.44 WAVES.
To generate numbers in games, we use the oracle - an external, non-blocking system. The server implements RSA-signature game id. The smart contract checks the validity of the signature and determines the winner. If the server did not send anything, then the user automatically wins.
The described method ensures that manipulation is technically impossible. All Tradisys games use an algorithm, so they are honest and transparent. Everything lends itself to public scrutiny. This ensures honesty.