Understanding Randomness in Blockchain Contracts
Randomness in the blockchain environment poses a unique challenge due to the need for all nodes to have the same "random" generator to achieve consensus. This challenge is addressed by employing Golang's standard seeded random number generator directly within the VM. You can find the relevant library here.
The VM function mBufferSetRandom
utilizes this library and is seeded with a combination of the following:
- Previous block random seed
- Current block random seed
- Transaction hash
We won't delve into the specifics of how the Golang library utilizes the seed or generates random numbers, as it goes beyond the scope of this tutorial.
Generating Random Numbers in Smart Contracts
The ManagedBuffer
type offers two methods for generating random numbers:
fn new_random(nr_bytes: usize) -> Self
, which creates a newManagedBuffer
withnr_bytes
random bytes.fn set_random(&mut self, nr_bytes: usize)
, which sets an existing buffer to contain random bytes.
For convenience, a wrapper named RandomnessSource
is provided. It includes methods for generating random numbers for all base Rust unsigned numerical types and a method for generating random bytes.
For example, to generate n
random u16
values:
let mut rand_source = RandomnessSource::new();
for _ in 0..n {
let my_rand_nr = rand_source.next_u16();
// Process the generated number
}
Similar methods are available for other Rust unsigned numerical types.
Generating Random Numbers within a Specific Range
Suppose you need to implement a Fisher-Yates shuffling algorithm within your smart contract (https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle). The RandomnessSource
struct provides methods for generating numbers within a specified range, such as fn next_usize_in_range(min: usize, max: usize) -> usize
, which generates a random usize
within the [min, max)
range. These methods are available for other numerical types as well.
For example, if you want to shuffle a vector of ManagedBuffer
s:
let mut my_vec = ManagedVec::new();
// ...
// Fill my_vec with elements
// ...
let vec_len = my_vec.len();
let mut rand_source = RandomnessSource::new();
for i in 0..vec_len {
let rand_index = rand_source.next_usize_in_range(i, vec_len);
let first_item = my_vec.get(i).unwrap();
let second_item = my_vec.get(rand_index).unwrap();
my_vec.set(i, &second_item);
my_vec.set(rand_index, &first_item);
}
This algorithm shuffles each element at position i
with an element within the range [i, vec_len)
.
Generating Random Bytes
Suppose you want to create NFTs in your contract and assign each of them a random 32-byte hash. To achieve this, you can use the next_bytes(len: usize)
method of the RandomnessSource
struct:
let mut rand_source = RandomnessSource::new();
let rand_hash = rand_source.next_bytes(32);
// Implement NFT creation logic here
Considerations
🚨 Avoid basing your smart contract logic solely on the current state. |
For example, this is an undesirable implementation:
#[payable("KLV")]
#[endpoint(rollDie)]
fn roll_die(&self) {
// ...
let payment = self.call_value().klv_value();
let rand_nr = rand_source.next_u8();
if rand_nr % 6 == 0 {
let prize = payment * 2u32;
self.send().direct(&caller, &prize);
}
// ...
}
Such logic can be easily abused by simulating transactions and only sending them when winning is guaranteed, resulting in a 100% win chance. Remember that you are operating on a public blockchain, not a private server.
A more robust implementation might look like this:
#[payable("KLV")]
#[endpoint(signUp)]
fn sign_up(&self) {
let already_signed_up = self.user_list().insert(caller.clone());
if already_signed_up {
sc_panic!("Already signed up");
}
}
#[only_owner]
#[endpoint(selectWinners)]
fn select_winners(&self) {
for user in self.user_list().iter() {
let rand_nr = rand_source.next_u8();
if rand_nr % 6 == 0 {
self.winners_list().insert(user.clone());
}
}
}
#[endpoint]
fn claim(&self) {
let was_winner = self.winners_list().swap_remove(&caller);
if was_winner {
self.send().direct_klv(&caller, &prize);
}
}
#[storage_mapper("userList")]
fn user_list(&self) -> UnorderedSetMapper<ManagedAddress>;
#[storage_mapper("winnersList")]
fn winners_list(&self) -> UnorderedSetMapper<ManagedAddress>;
Conclusion
The provided random number generator should suffice for most use cases. Enjoy using it for lotteries and other applications!