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 new ManagedBuffer with nr_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 ManagedBuffers:

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!

Was this page helpful?