Generation of block random seed
This information is accurate at the time of writing. It may change during any network upgrade.
Lottery contracts occasionally appear on TON. These contracts often use unsafe methods to handle randomness, making the generated values predictable and allowing the lottery to be exploited.
Exploiting weaknesses in random number generation typically involves using a proxy contract that forwards a message if the random value meets specific conditions. While proposals exist for wallet contracts that can execute arbitrary on-chain code (specified and signed by the user), most popular wallet versions do not support this functionality. So, if a lottery checks whether a gambler participates through a wallet contract, is it safe?
Alternatively, the question can be framed as: Can an external message be included in a block where the random value matches the sender's requirements?
The sender cannot influence randomness directly. However, validators generating blocks and including proposed external messages can.
How validators affect the seed
Limited information about this topic, even in whitepapers, confuses developers. The TON Whitepaper mentions block randomness briefly:
The algorithm used to select validator task groups for each shard (w, s) is deterministic pseudorandom. It uses pseudorandom numbers embedded by validators into each MasterChain block (generated by a consensus using threshold signatures) to create a random seed, and then computes, for example, Hash(code(w). code(s).validator_id.rand_seed) for each validator.
However, the most reliable and up-to-date source is the code itself. Let's examine collator.cpp:
{
// generate rand seed
prng::rand_gen().strong_rand_bytes(rand_seed->data(), 32);
LOG(DEBUG) << "block random seed set to " << rand_seed->to_hex();
}
This code generates the random seed for a block. It resides in the collator code because the party generating blocks requires it, while lite validators do not.
A single validator or collator generates the seed when creating a block. This raises the following question:
Can the decision to include an external message be made after the seed is known?
Yes, it can. Here’s why: if the system imports an external message, its execution must succeed. Since execution can depend on random values, the block seed must be known beforehand.
Thus, there is a way to exploit "unsafe" (single-block) randomness if the sender collaborates with a validator. Even if the contract uses randomize_lt()
, the validator can generate a suitable seed or include the proposed external message in a block that meets all conditions. A validator acting in this way would still be considered fair. This is the essence of decentralization.
To fully cover randomness, let's address one more question.
How does the block seed affect randomness in contracts?
The seed generated by the validator is not used directly in all contracts. Instead, it is hashed with the account address.
bool Transaction::prepare_rand_seed(td::BitArray<256>& rand_seed, const ComputePhaseConfig& cfg) const {
// we might use SHA256(block_rand_seed . addr . trans_lt)
// instead, we use SHA256(block_rand_seed . addr)
// if the smart contract wants to randomize further, it can use RANDOMIZE instruction
td::BitArray<256 + 256> data;
data.bits().copy_from(cfg.block_rand_seed.cbits(), 256);
(data.bits() + 256).copy_from(account.addr_rewrite.cbits(), 256);
rand_seed.clear();
data.compute_sha256(rand_seed);
return true;
}
Pseudorandom numbers are then generated using the procedure described on the TVM instructions page:
x{F810} RANDU256
Generates a new pseudorandom unsigned 256-bit Integer x. The algorithm is as follows: if r is the old value of the random seed, considered as a 32-byte array (by constructing the big-endian representation of an unsigned 256-bit integer), then its sha512(r) is computed; the first 32 bytes of this hash are stored as the new value r' of the random seed, and the remaining 32 bytes are returned as the next random value x.
This process is confirmed by examining the code for preparing the contract's c7 (c7 is a tuple for temporary data, storing the contract address, starting balance, random seed, etc.) and generating random values.
Conclusion
No random number generation in TON is entirely safe in terms of unpredictability. This means no perfect lottery can exist on TON, nor can any lottery be fully trusted to be fair.
Typical usage of pseudorandom number generators (PRNGs) may include randomize_lt()
, but such contracts can still be tricked by selecting the correct blocks to send messages. Proposed solutions, such as sending messages to another workchain and receiving a response to skip blocks, only delay the threat. In reality, any validator (representing 1/250 of the TON Blockchain) can choose the optimal time to send a request to a lottery contract so that the response arrives in a block they generate. They can then select any block seed they desire. This risk will increase once collators are introduced to the mainnet, as standard complaints cannot fine them since they do not stake anything in the Elector contract.