Cryptographically Secure RNG for Motoko

Hi everyone,

While working on my upcoming project/game (to be announced soon), I ran into a challenge where I needed thousands of unpredictable random numbers. The existing PRNGs weren’t suitable, as they could be easily reverse-engineered and predicted using tools like Z3Prover. Motoko’s built-in random module would provide secure and unpredictable results, but it’s not feasible in my case, as it takes several seconds per Random.blob() call, which isn’t manageable when generating thousands of random numbers.

To solve this, I decided to implement a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG). After evaluating several algorithms, I chose the ChaCha algorithm for its balance of speed and security—ideal for my case for generating a 2D world and hiding rewards, where randomness must be both unpredictable and efficient.

If you’re interested, here are the resources:

Feel free to dive in and adapt it for your own projects.
Let me know if you have any questions or suggestions!

7 Likes

Very cool. Thanks for making this!

One suggestion, if you want you can add a benchmark file. Then mops will run it and show us how many instructions are required to produce say 1,000 random numbers.

Hi @timo. Thanks for kind words.
Just added benchmarking to the package:
https://mops.one/csprng/benchmarks

Does this still use the base IC random beacon as a seed?

Thanks. I can see the benchmarks now.

One more question: what’s the source of the test vectors? Are they standard vectors, from the ChaCha specification? Or did you generate them? If the latter with which software? You could put that as a comment in the test file, so that people know how they can reproduce the test vectors.

BTW, for getting random numbers from a range this approach is not allowed.

return min + (number % rangeSize);

Using modulo introduces bias. You have to discard values that are out of range and take a fresh new value.

1 Like

The RNG package expects two blobs as input: a 32-byte blob for the key and a 12-byte blob for the nonce. It does not require the base IC random beacon specifically, so you can provide any blobs. However, if the project is open-sourced, anyone could potentially view the seed, which would allow them to predict the RNG outcome.

For security and fairness, I used the IC random beacon to generate the key and nonce blobs in the Motoko Playground example, ensuring an unpredictable seed. I apply the same approach in my game for 2D world generation and reward hiding, obtaining a random seed on new canister/world deployment so that even I can’t predict or influence the RNG outcome.

So, I believe using the IC random beacon for the initial seed and nonce is a secure and reliable choice, as it provides strong randomness while being called only once during RNG initialization. This single call to the IC beacon ensures an unpredictable seed without needing repeated beacon calls, making it efficient and secure.

Thank you @timo for your insightful questions—they really prompted me to dive deeper into refining my RNG package. Your feedback led me to remove the modulo bias and implement detailed test vectors from RFC 8439, Section 2.3.2, which validate the accuracy of the implementation.
Thanks to your feedback, I’m now confident that everything is functioning as it should.
I appreciate your help in guiding me toward these improvements!

What is the lifetime of the CSPRNG?

Do you get the random beacon once, initialize a RNG with it and then consume the RNG in one message execution?

Or do you get the random beacon only once ever, initialize a RNG with it and then keep using the same RNG across multiple message execution?

Package itself doesn’t use random beacon, but in Motoko Playground example (provided in documentation) The CSPRNG is initialized only once with a random seed from the IC random beacon, which is obtained at the time of the RNG’s creation. After this single initialization, the same RNG instance is reused across multiple message executions, allowing for consistent randomness without repeated calls to the IC random beacon.

By using the IC random beacon only once at initialization, I ensure that the RNG maintains its state across message executions. This approach provides strong randomness while avoiding unnecessary beacon calls, making it efficient for ongoing use.

Thanks for adding the test vectors!

For the numbers range it looks like you go to the next byte border above the range size. You should go to the next bit border. That way you have in the worst case a 1/2 chance for each iteration of the loop to be successful. If you go to the next byte border then in the worst case you have a 1/256 chance, hence you will need a lot of iterations.

For example think of the range [0,256] which has range size 2^8 + 1 and you are sampling from all 2-byte numbers. You should sample 2-byte numbers but then mask them to take only 9 bits of them, then check if the result is <= 256.

1 Like

You’re absolutely right—previously, this step wasn’t essential, but with the addition of rejection sampling to avoid modulo bias, it’s become crucial for optimization.
Great catch! Thanks again for the helpful feedback! :wink:

I optimized the package, and the improvements are pretty significant:

  • Instruction count decreased by 37%
  • Garbage Collection time decreased by 42%

Now, I’m bit embarrassed how inefficient my earlier version was. :blush:

Feel free to share any feedback or suggestions for further optimization!

2 Likes