Random number generation
Generating random numbers is a common task that you may need in many different projects. You might have already seen the random()
function in FunC docs, but note that its result can be easily predicted unless you employ some additional tricks.
How can someone predict a random number?
Computers are terrible at generating random information because all they do is follow the instructions of users. However, since people frequently need random numbers, they've devised various methods for generating pseudo-random numbers.
These algorithms typically require you to provide a seed value that will be used to generate a sequence of pseudo-random numbers. So, if you run the same program with the same seed multiple times, you'll consistently get the same result. In TON, the seed is different for each block.
Therefore, to predict the result of the random()
function in a smart contract, you just need to know the current seed
of the block, which isn't possible if you're not a validator.
Simply use randomize_lt()
To make the random number generation unpredictable, you can add the current Logical Time to the seed, so different transactions will have different seeds and results.
Just add the randomize_lt()
call before generating random numbers, and your random numbers will become unpredictable:
randomize_lt();
int x = random(); ;; users can't predict this number
However, you should note that validators or collators may still affect the result of the random number, as they determine the seed of the current block.
Is there a way to protect against manipulation by validators?
To prevent (or at least complicate) the substitution of the seed by validators, you can use more complex schemes. For instance, you could skip one block before generating a random number. If we skip a block, the seed will change in a less predictable manner.
Skipping blocks isn't a complex task. You can do it by simply sending a message to the Masterchain and back to the workchain of your contract. Let's examine a simple example!
Do not use this example contract in real projects, write your own instead.
Masterchain echo-contract
The purpose of this contract is just to forward the message back to the sender. This can be accomplished in a few FunC lines of code:
() recv_internal (cell in_msg_full, slice in_msg_body) impure {
var cs = in_msg_full.begin_parse();
var flags = cs~load_uint(4);
if (flags & 1) { ;; ignore bounced messages
return ();
}
slice sender = cs~load_msg_addr();
send_raw_message(
begin_cell()
.store_uint(0x18, 6)
.store_slice(sender)
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(in_msg_body)
.end_cell(),
64 + 2 ;; send the remaining value of an incoming msg & ignore errors
);
}
Just deploy this contract in the Masterchain, and let's move to the main contract.
Main contract in any workchain
Let's write a simple lottery contract as an example. A user will send 1 TON to it, and with a 50% chance, will get 2 TON back.
;; set the echo-contract address
const echo_address = "Ef8Nb7157K5bVxNKAvIWreRcF0RcUlzcCA7lwmewWVNtqM3s"a;
() recv_internal (int msg_value, cell in_msg_full, slice in_msg_body) impure {
var cs = in_msg_full.begin_parse();
var flags = cs~load_uint(4);
if (flags & 1) { ;; ignore bounced messages
return ();
}
slice sender = cs~load_msg_addr();
int op = in_msg_body~load_uint(32);
if ((op == 0) & equal_slice_bits(in_msg_body, "bet")) { ;; bet from user
throw_unless(501, msg_value == 1000000000); ;; 1 TON
send_raw_message(
begin_cell()
.store_uint(0x18, 6)
.store_slice(echo_address)
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(1, 32) ;; let 1 be echo opcode in our contract
.store_slice(sender) ;; forward user address
.end_cell(),
64 ;; send the remaining value of an incoming msg
);
}
elseif (op == 1) { ;; echo
throw_unless(502, equal_slice_bits(sender, echo_address)); ;; only accept echoes from our echo-contract
slice user = in_msg_body~load_msg_addr();
{-
at this point we have skipped 1+ blocks
so let's just generate the random number
-}
randomize_lt();
int x = rand(2); ;; generate a random number (either 0 or 1)
if (x == 1) { ;; user won
send_raw_message(
begin_cell()
.store_uint(0x18, 6)
.store_slice(user)
.store_coins(2000000000) ;; 2 TON
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.end_cell(),
3 ;; ignore errors & pay fees separately
);
}
}
}
Deploy this contract in any workchain you need (probably Basechain) and you're done!
Is this method 100% secure?
While it certainly helps, there's still a chance of manipulation if an intruder has control over several validators simultaneously. In this case, they might, with some probability, affect the seed, which the random number depends on. Even if this probability is extremely small, it's still worth considering.
With the latest TVM upgrade, the introduction of new values to the c7
register can further boost the security of random number generation. Specifically, the upgrade adds information about the last 16 masterchain blocks to the c7
register.
The masterchain block information, due to its constantly changing nature, can serve as an additional source of entropy for random number generation. By incorporating this data into your randomness algorithm, you can create numbers that are even harder for potential adversaries to predict.
For more detailed information on this TVM upgrade, please refer to TVM Upgrade.