There’s a problem we needed to address in the course of developing our games. It’s complicated to generate a random number in a distributed network. Almost all blockchains have already faced this issue. Indeed, in networks where there is no trust between anyone, the creation of a random number solves a wide range of problems.
In this article, we explain how we solved this problem for our games. The first of these was Waves Xmas Tree.
Initially, we planned to generate a number using information from the blockchain. However, on further investigation, it became clear that the process used to create a number this way could be manipulated. We had to discard this solution.
We came up with a workaround, using a ‘commit-reveal’ scheme. The server proposed a number from 1 to 5, added ‘salt’ to it and hashed the result using the Keccak function. The server pre-debugged a smart contract with an already saved number. The result was that the game was effectively reduced to the user guessing the number hidden by the hash.
The player placed their bet and the server sent a hidden number and ‘salt’ to a smart contract. To put it another way, the cards were revealed. Afterwards, the server verified the numbers and decided whether the user had won or lost.
If the server didn’t send the number and ‘salt’ for verification, then the user won. In this case, it was necessary to deploy a smart contract in advance and arrange potential winnings for each game. This was inconvenient, expensive and time-consuming. At that time, though, there was no other secure solution.
Shortly afterwards, the Tradisys team proposed adding the rsaVerify() function to the Waves protocol. This checks the validity of an RSA signature based on public and private keys. As a result of our proposal, the function was added.
We built three new games: Dice Roller, Coin Flip and Ride On Waves. In each of them, the new random number technology was implemented. Let’s take a closer look at how it works.
Let’s look at the random number generation first. You can find the smart contract here.
Go to the Script tab and choose Decompiled. You will see the smart contract’s code (or script).
The smart contract code consists of a list of functions. The ones that are @Callable can be run via Invocation transactions. We are interested in two of them: bet and withdraw:
- func bet (playerChoice)
- func withdraw (gameId, rsaSign)
- The user chooses the range and bet size.
- The client arranges the bet function. For the image above it would be bet («50»)
- The client sends an Invocation transaction to the smart contract address (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 on the smart contract (choice: String).
4. Let’s look at 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 records a new game in the smart contract state:
- Unique new game id (game id)
- Game state = SUBMITTED
- Player choice (the range is 50)
- Public key
- Potential reward (depends on the player’s bet)
This is how the key-value database looks on the blockchain:
"type": "string",
"value": "03WON_0283_448t8Jn9P3717UnXFEVD5VWjfeGE5gBNeWg58H2aJeQEgJ_06574069_09116020000_0229",
"key": "2GKTX6NLTgUrE4iy9HtpSSHpZ3G8W4cMfdjyvvnc21dx"
‘Key’ is the game id for a new game. The remaining data is contained in the field ‘value’. These entries are stored in the Data tab of the smart contract:
5. The server finds the sent transaction (the new game) via blockchain API. Game id is already recorded in the blockchain, so it’s impossible to change or delete it.
6. The server forms a withdraw function (gameId, rsaSign) such as:
7. The server sends an Invocation transaction to the smart contract (broadcast InvocationTx). The transaction contains a call to the generated withdraw function (gameId, rsaSign):
The function contains game id and an RSA signature of a unique id. The signature result is unchangeable.
What does this mean?
We take the same value (game id) and apply the RSA signature method to it. This is how the RSA algorithm works. It’s impossible to manipulate the final number because game id and the result of the RSA algorithm are unknown. It’s also pointless to try to guess a number.
8. The blockchain receives a transaction that runs the withdraw function (gameId, rsaSign).
9. There is a call for the GenerateRandIn function inside the withdraw function (gameId, rsaSign). 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 is a random number
Firstly, the string that is a result of the RSA signature is taken. Then, it is hashed via SHA-256 (sha256(rsaSign)).
We can’t predict the signature result and subsequent hashing. Thus, it is impossible to affect its generation. To get a number in a specific range (e.g. from 1 to 100), the conversion functions toInt and % 100 (mod analogue) are applied.
At the beginning of the article, we mentioned the rsaVerify() function that allows checking of the validity of an RSA signature by a private key against a public one. Here is a part of GenerateRandInt (gameId, rsaSign):
rsaVerify (SHA256, toBytes(gameId), rsaSign, RSAPUBLIC)
To start with, RSAPUBLIC public key and rsaSign string are taken. The signature is checked for validity. If the check is successful, the number is generated. Otherwise, the system considers that the signature is not valid (Invalid RSA signature).
The server has to sign the game id using a private key and send a valid RSA signature within 2,880 blocks. The option is managed while the smart contract is deploying. If nothing happens in the stated time, the user wins. In this case, the reward has to be sent by the user independently. It turns out that cheating is unprofitable for the server because this leads to a loss. There is an example below.
The user plays Dice Roller. He chooses 2 of 6 cube faces, with a bet of 14 WAVES. If the server does not send a valid RSA signature to the smart contract within a set time (2,880 blocks), the user will receive 34.44 WAVES.
For number generation, we use an oracle, an external system rather than the blockchain. The server implements an RSA signature for the game id. The smart contract checks signature validity and determines the winner. If the server sends nothing, then the user would win automatically.
This method ensures that manipulation is technically impossible. All Tradisys games are based on the algorithm described above — ensuring our games are fair and transparent. Everything can be publicly audited to ensure honesty.
Read Waves News channel
Follow Waves Twitter
Watch Waves Youtube
Subscribe to Waves Subreddit
Blockchain RSA-based random was originally published in Waves Platform on Medium, where people are continuing the conversation by highlighting and responding to this story.