diff --git a/crates/sui-framework/docs/sui-framework/hmac.md b/crates/sui-framework/docs/sui-framework/hmac.md index cf5dbeea4418e..1bc40a4c1feb2 100644 --- a/crates/sui-framework/docs/sui-framework/hmac.md +++ b/crates/sui-framework/docs/sui-framework/hmac.md @@ -21,7 +21,7 @@ title: Module `0x2::hmac` Returns the 32 bytes digest of HMAC-SHA3-256(key, msg). -
public fun hmac_sha3_256(key: &vector<u8>, msg: &vector<u8>): vector<u8>
+public fun hmac_sha3_256(key: &vector<u8>, msg: &vector<u8>): vector<u8>
@@ -30,7 +30,7 @@ Returns the 32 bytes digest of HMAC-SHA3-256(key, msg).
Implementation
-public native fun hmac_sha3_256(key: &vector<u8>, msg: &vector<u8>): vector<u8>;
+public native fun hmac_sha3_256(key: &vector<u8>, msg: &vector<u8>): vector<u8>;
diff --git a/crates/sui-framework/docs/sui-framework/random.md b/crates/sui-framework/docs/sui-framework/random.md
index 451f5d306bd9d..a00c385f2b753 100644
--- a/crates/sui-framework/docs/sui-framework/random.md
+++ b/crates/sui-framework/docs/sui-framework/random.md
@@ -3,18 +3,42 @@
title: Module `0x2::random`
---
+This module provides functionality for generating secure randomness.
- [Resource `Random`](#0x2_random_Random)
- [Struct `RandomInner`](#0x2_random_RandomInner)
+- [Struct `RandomGenerator`](#0x2_random_RandomGenerator)
- [Constants](#@Constants_0)
- [Function `create`](#0x2_random_create)
- [Function `load_inner_mut`](#0x2_random_load_inner_mut)
- [Function `load_inner`](#0x2_random_load_inner)
- [Function `update_randomness_state`](#0x2_random_update_randomness_state)
-
-
-use 0x1::vector;
+- [Function `new_generator`](#0x2_random_new_generator)
+- [Function `derive_next_block`](#0x2_random_derive_next_block)
+- [Function `fill_buffer`](#0x2_random_fill_buffer)
+- [Function `generate_bytes`](#0x2_random_generate_bytes)
+- [Function `u256_from_bytes`](#0x2_random_u256_from_bytes)
+- [Function `generate_u256`](#0x2_random_generate_u256)
+- [Function `generate_u128`](#0x2_random_generate_u128)
+- [Function `generate_u64`](#0x2_random_generate_u64)
+- [Function `generate_u32`](#0x2_random_generate_u32)
+- [Function `generate_u16`](#0x2_random_generate_u16)
+- [Function `generate_u8`](#0x2_random_generate_u8)
+- [Function `generate_bool`](#0x2_random_generate_bool)
+- [Function `u128_in_range`](#0x2_random_u128_in_range)
+- [Function `generate_u128_in_range`](#0x2_random_generate_u128_in_range)
+- [Function `generate_u64_in_range`](#0x2_random_generate_u64_in_range)
+- [Function `generate_u32_in_range`](#0x2_random_generate_u32_in_range)
+- [Function `generate_u16_in_range`](#0x2_random_generate_u16_in_range)
+- [Function `generate_u8_in_range`](#0x2_random_generate_u8_in_range)
+- [Function `shuffle`](#0x2_random_shuffle)
+
+
+use 0x1::bcs;
+use 0x1::vector;
+use 0x2::address;
+use 0x2::hmac;
use 0x2::object;
use 0x2::transfer;
use 0x2::tx_context;
@@ -101,6 +125,46 @@ The actual state is stored in a versioned inner field.
+
+
+
+
+## Struct `RandomGenerator`
+
+Unique randomness generator, derived from the global randomness.
+
+
+struct RandomGenerator has drop
+
+
+
+
+
+Fields
+
+
+
+-
+
seed: vector<u8>
+
+-
+
+
+-
+
counter: u16
+
+-
+
+
+-
+
buffer: vector<u8>
+
+-
+
+
+
+
+
@@ -135,6 +199,15 @@ The actual state is stored in a versioned inner field.
+
+
+
+
+const EInvalidLength: u64 = 4;
+
+
+
+
@@ -144,6 +217,33 @@ The actual state is stored in a versioned inner field.
+
+
+
+
+const EInvalidRange: u64 = 3;
+
+
+
+
+
+
+
+
+const RAND_OUTPUT_LEN: u16 = 32;
+
+
+
+
+
+
+
+
+const U16_MAX: u64 = 65535;
+
+
+
+
## Function `create`
@@ -280,7 +380,7 @@ transaction.
let epoch = tx_context::epoch(ctx);
let inner = load_inner_mut(self);
if (inner.randomness_round == 0 && inner.epoch == 0 &&
- vector::is_empty(&inner.random_bytes)) {
+ vector::is_empty(&inner.random_bytes)) {
// First update should be for round zero.
assert!(new_round == 0, EInvalidRandomnessUpdate);
} else {
@@ -302,4 +402,528 @@ transaction.
+
+
+
+
+## Function `new_generator`
+
+Create a generator. Can be used to derive up to MAX_U16 * 32 random bytes.
+
+
+public fun new_generator(r: &random::Random, ctx: &mut tx_context::TxContext): random::RandomGenerator
+
+
+
+
+
+Implementation
+
+
+public fun new_generator(r: &Random, ctx: &mut TxContext): RandomGenerator {
+ let inner = load_inner(r);
+ let seed = hmac_sha3_256(
+ &inner.random_bytes,
+ &to_bytes(fresh_object_address(ctx))
+ );
+ RandomGenerator { seed, counter: 0, buffer: vector[] }
+}
+
+
+
+
+
+
+
+
+## Function `derive_next_block`
+
+
+
+fun derive_next_block(g: &mut random::RandomGenerator): vector<u8>
+
+
+
+
+
+Implementation
+
+
+fun derive_next_block(g: &mut RandomGenerator): vector<u8> {
+ g.counter = g.counter + 1;
+ hmac_sha3_256(&g.seed, &bcs::to_bytes(&g.counter))
+}
+
+
+
+
+
+
+
+
+## Function `fill_buffer`
+
+
+
+fun fill_buffer(g: &mut random::RandomGenerator)
+
+
+
+
+
+Implementation
+
+
+fun fill_buffer(g: &mut RandomGenerator) {
+ let next_block = derive_next_block(g);
+ vector::append(&mut g.buffer, next_block);
+}
+
+
+
+
+
+
+
+
+## Function `generate_bytes`
+
+Generate n random bytes.
+
+
+public fun generate_bytes(g: &mut random::RandomGenerator, num_of_bytes: u16): vector<u8>
+
+
+
+
+
+Implementation
+
+
+public fun generate_bytes(g: &mut RandomGenerator, num_of_bytes: u16): vector<u8> {
+ let mut result = vector[];
+ // Append RAND_OUTPUT_LEN size buffers directly without going through the generator's buffer.
+ let mut num_of_blocks = num_of_bytes / RAND_OUTPUT_LEN;
+ while (num_of_blocks > 0) {
+ vector::append(&mut result, derive_next_block(g));
+ num_of_blocks = num_of_blocks - 1;
+ };
+ // Fill the generator's buffer if needed.
+ let num_of_bytes = (num_of_bytes as u64);
+ if (vector::length(&g.buffer) < (num_of_bytes - vector::length(&result))) {
+ fill_buffer(g);
+ };
+ // Take remaining bytes from the generator's buffer.
+ while (vector::length(&result) < num_of_bytes) {
+ vector::push_back(&mut result, vector::pop_back(&mut g.buffer));
+ };
+ result
+}
+
+
+
+
+
+
+
+
+## Function `u256_from_bytes`
+
+
+
+fun u256_from_bytes(g: &mut random::RandomGenerator, num_of_bytes: u8): u256
+
+
+
+
+
+Implementation
+
+
+fun u256_from_bytes(g: &mut RandomGenerator, num_of_bytes: u8): u256 {
+ if (vector::length(&g.buffer) < (num_of_bytes as u64)) {
+ fill_buffer(g);
+ };
+ let mut result: u256 = 0;
+ let mut i = 0;
+ while (i < num_of_bytes) {
+ let byte = vector::pop_back(&mut g.buffer);
+ result = (result << 8) + (byte as u256);
+ i = i + 1;
+ };
+ result
+}
+
+
+
+
+
+
+
+
+## Function `generate_u256`
+
+Generate a u256.
+
+
+public fun generate_u256(g: &mut random::RandomGenerator): u256
+
+
+
+
+
+Implementation
+
+
+public fun generate_u256(g: &mut RandomGenerator): u256 {
+ u256_from_bytes(g, 32)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u128`
+
+Generate a u128.
+
+
+public fun generate_u128(g: &mut random::RandomGenerator): u128
+
+
+
+
+
+Implementation
+
+
+public fun generate_u128(g: &mut RandomGenerator): u128 {
+ (u256_from_bytes(g, 16) as u128)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u64`
+
+Generate a u64.
+
+
+public fun generate_u64(g: &mut random::RandomGenerator): u64
+
+
+
+
+
+Implementation
+
+
+public fun generate_u64(g: &mut RandomGenerator): u64 {
+ (u256_from_bytes(g, 8) as u64)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u32`
+
+Generate a u32.
+
+
+public fun generate_u32(g: &mut random::RandomGenerator): u32
+
+
+
+
+
+Implementation
+
+
+public fun generate_u32(g: &mut RandomGenerator): u32 {
+ (u256_from_bytes(g, 4) as u32)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u16`
+
+Generate a u16.
+
+
+public fun generate_u16(g: &mut random::RandomGenerator): u16
+
+
+
+
+
+Implementation
+
+
+public fun generate_u16(g: &mut RandomGenerator): u16 {
+ (u256_from_bytes(g, 2) as u16)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u8`
+
+Generate a u8.
+
+
+public fun generate_u8(g: &mut random::RandomGenerator): u8
+
+
+
+
+
+Implementation
+
+
+public fun generate_u8(g: &mut RandomGenerator): u8 {
+ (u256_from_bytes(g, 1) as u8)
+}
+
+
+
+
+
+
+
+
+## Function `generate_bool`
+
+Generate a boolean.
+
+
+public fun generate_bool(g: &mut random::RandomGenerator): bool
+
+
+
+
+
+Implementation
+
+
+public fun generate_bool(g: &mut RandomGenerator): bool {
+ (u256_from_bytes(g, 1) & 1) == 1
+}
+
+
+
+
+
+
+
+
+## Function `u128_in_range`
+
+
+
+fun u128_in_range(g: &mut random::RandomGenerator, min: u128, max: u128, num_of_bytes: u8): u128
+
+
+
+
+
+Implementation
+
+
+fun u128_in_range(g: &mut RandomGenerator, min: u128, max: u128, num_of_bytes: u8): u128 {
+ assert!(min <= max, EInvalidRange);
+ if (min == max) {
+ return min
+ };
+ // Pick a random number in [0, max - min] by generating a random number that is larger than max-min, and taking
+ // the modulo of the random number by the range size. Then add the min to the result to get a number in
+ // [min, max].
+ let range_size = ((max - min) as u256) + 1;
+ let rand = u256_from_bytes(g, num_of_bytes);
+ min + ((rand % range_size) as u128)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u128_in_range`
+
+Generate a random u128 in [min, max] (with a bias of 2^{-64}).
+
+
+public fun generate_u128_in_range(g: &mut random::RandomGenerator, min: u128, max: u128): u128
+
+
+
+
+
+Implementation
+
+
+public fun generate_u128_in_range(g: &mut RandomGenerator, min: u128, max: u128): u128 {
+ u128_in_range(g, min, max, 24)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u64_in_range`
+
+
+
+public fun generate_u64_in_range(g: &mut random::RandomGenerator, min: u64, max: u64): u64
+
+
+
+
+
+Implementation
+
+
+public fun generate_u64_in_range(g: &mut RandomGenerator, min: u64, max: u64): u64 {
+ (u128_in_range(g, (min as u128), (max as u128), 16) as u64)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u32_in_range`
+
+Generate a random u32 in [min, max] (with a bias of 2^{-64}).
+
+
+public fun generate_u32_in_range(g: &mut random::RandomGenerator, min: u32, max: u32): u32
+
+
+
+
+
+Implementation
+
+
+public fun generate_u32_in_range(g: &mut RandomGenerator, min: u32, max: u32): u32 {
+ (u128_in_range(g, (min as u128), (max as u128), 12) as u32)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u16_in_range`
+
+Generate a random u16 in [min, max] (with a bias of 2^{-64}).
+
+
+public fun generate_u16_in_range(g: &mut random::RandomGenerator, min: u16, max: u16): u16
+
+
+
+
+
+Implementation
+
+
+public fun generate_u16_in_range(g: &mut RandomGenerator, min: u16, max: u16): u16 {
+ (u128_in_range(g, (min as u128), (max as u128), 10) as u16)
+}
+
+
+
+
+
+
+
+
+## Function `generate_u8_in_range`
+
+Generate a random u8 in [min, max] (with a bias of 2^{-64}).
+
+
+public fun generate_u8_in_range(g: &mut random::RandomGenerator, min: u8, max: u8): u8
+
+
+
+
+
+Implementation
+
+
+public fun generate_u8_in_range(g: &mut RandomGenerator, min: u8, max: u8): u8 {
+ (u128_in_range(g, (min as u128), (max as u128), 9) as u8)
+}
+
+
+
+
+
+
+
+
+## Function `shuffle`
+
+Shuffle a vector using the random generator (Fisher–Yates/Knuth shuffle).
+
+
+public fun shuffle<T>(g: &mut random::RandomGenerator, v: &mut vector<T>)
+
+
+
+
+
+Implementation
+
+
+public fun shuffle<T>(g: &mut RandomGenerator, v: &mut vector<T>) {
+ let n = vector::length(v);
+ if (n == 0) {
+ return
+ };
+ assert!(n <= U16_MAX, EInvalidLength);
+ let n = (n as u16);
+ let mut i: u16 = 0;
+ let end = n - 1;
+ while (i < end) {
+ let j = generate_u16_in_range(g, i, end);
+ vector::swap(v, (i as u64), (j as u64));
+ i = i + 1;
+ };
+}
+
+
+
+
diff --git a/crates/sui-framework/packages/sui-framework/sources/random.move b/crates/sui-framework/packages/sui-framework/sources/random.move
index bb5feb8d227b4..e8f7b1313d0f0 100644
--- a/crates/sui-framework/packages/sui-framework/sources/random.move
+++ b/crates/sui-framework/packages/sui-framework/sources/random.move
@@ -1,34 +1,39 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
-#[allow(unused_use)]
-// This module provides functionality for generating and using secure randomness.
-//
-// Randomness is currently write-only, until user-facing API is implemented.
+/// This module provides functionality for generating secure randomness.
module sui::random {
+ use std::bcs;
use std::vector;
+ use sui::address::to_bytes;
+ use sui::hmac::hmac_sha3_256;
use sui::object::{Self, UID};
use sui::transfer;
- use sui::tx_context::{Self, TxContext};
+ use sui::tx_context::{Self, TxContext, fresh_object_address};
use sui::versioned::{Self, Versioned};
// Sender is not @0x0 the system address.
const ENotSystemAddress: u64 = 0;
const EWrongInnerVersion: u64 = 1;
const EInvalidRandomnessUpdate: u64 = 2;
+ const EInvalidRange: u64 = 3;
+ const EInvalidLength: u64 = 4;
const CURRENT_VERSION: u64 = 1;
+ const RAND_OUTPUT_LEN: u16 = 32;
+ const U16_MAX: u64 = 0xFFFF;
/// Singleton shared object which stores the global randomness state.
/// The actual state is stored in a versioned inner field.
public struct Random has key {
id: UID,
+ // The inner object must never be accessed outside this module as it could be used for accessing global
+ // randomness via deserialization of RandomInner.
inner: Versioned,
}
public struct RandomInner has store {
version: u64,
-
epoch: u64,
randomness_round: u64,
random_bytes: vector,
@@ -74,7 +79,6 @@ module sui::random {
inner
}
- #[allow(unused_function)] // TODO: remove annotation after implementing user-facing API
fun load_inner(
self: &Random,
): &RandomInner {
@@ -103,7 +107,7 @@ module sui::random {
let epoch = tx_context::epoch(ctx);
let inner = load_inner_mut(self);
if (inner.randomness_round == 0 && inner.epoch == 0 &&
- vector::is_empty(&inner.random_bytes)) {
+ vector::is_empty(&inner.random_bytes)) {
// First update should be for round zero.
assert!(new_round == 0, EInvalidRandomnessUpdate);
} else {
@@ -131,4 +135,181 @@ module sui::random {
) {
update_randomness_state(self, new_round, new_bytes, ctx);
}
+
+
+ /// Unique randomness generator, derived from the global randomness.
+ public struct RandomGenerator has drop {
+ seed: vector,
+ counter: u16,
+ buffer: vector,
+ }
+
+ /// Create a generator. Can be used to derive up to MAX_U16 * 32 random bytes.
+ public fun new_generator(r: &Random, ctx: &mut TxContext): RandomGenerator {
+ let inner = load_inner(r);
+ let seed = hmac_sha3_256(
+ &inner.random_bytes,
+ &to_bytes(fresh_object_address(ctx))
+ );
+ RandomGenerator { seed, counter: 0, buffer: vector[] }
+ }
+
+ // Get the next block of random bytes.
+ fun derive_next_block(g: &mut RandomGenerator): vector {
+ g.counter = g.counter + 1;
+ hmac_sha3_256(&g.seed, &bcs::to_bytes(&g.counter))
+ }
+
+ // Fill the generator's buffer with 32 random bytes.
+ fun fill_buffer(g: &mut RandomGenerator) {
+ let next_block = derive_next_block(g);
+ vector::append(&mut g.buffer, next_block);
+ }
+
+ /// Generate n random bytes.
+ public fun generate_bytes(g: &mut RandomGenerator, num_of_bytes: u16): vector {
+ let mut result = vector[];
+ // Append RAND_OUTPUT_LEN size buffers directly without going through the generator's buffer.
+ let mut num_of_blocks = num_of_bytes / RAND_OUTPUT_LEN;
+ while (num_of_blocks > 0) {
+ vector::append(&mut result, derive_next_block(g));
+ num_of_blocks = num_of_blocks - 1;
+ };
+ // Fill the generator's buffer if needed.
+ let num_of_bytes = (num_of_bytes as u64);
+ if (vector::length(&g.buffer) < (num_of_bytes - vector::length(&result))) {
+ fill_buffer(g);
+ };
+ // Take remaining bytes from the generator's buffer.
+ while (vector::length(&result) < num_of_bytes) {
+ vector::push_back(&mut result, vector::pop_back(&mut g.buffer));
+ };
+ result
+ }
+
+ // Helper function that extracts the given number of bytes from the random generator and returns it as u256.
+ // Assumes that the caller has already checked that num_of_bytes is valid.
+ // TODO: Replace with a macro when we have support for it.
+ fun u256_from_bytes(g: &mut RandomGenerator, num_of_bytes: u8): u256 {
+ if (vector::length(&g.buffer) < (num_of_bytes as u64)) {
+ fill_buffer(g);
+ };
+ let mut result: u256 = 0;
+ let mut i = 0;
+ while (i < num_of_bytes) {
+ let byte = vector::pop_back(&mut g.buffer);
+ result = (result << 8) + (byte as u256);
+ i = i + 1;
+ };
+ result
+ }
+
+ /// Generate a u256.
+ public fun generate_u256(g: &mut RandomGenerator): u256 {
+ u256_from_bytes(g, 32)
+ }
+
+ /// Generate a u128.
+ public fun generate_u128(g: &mut RandomGenerator): u128 {
+ (u256_from_bytes(g, 16) as u128)
+ }
+
+ /// Generate a u64.
+ public fun generate_u64(g: &mut RandomGenerator): u64 {
+ (u256_from_bytes(g, 8) as u64)
+ }
+
+ /// Generate a u32.
+ public fun generate_u32(g: &mut RandomGenerator): u32 {
+ (u256_from_bytes(g, 4) as u32)
+ }
+
+ /// Generate a u16.
+ public fun generate_u16(g: &mut RandomGenerator): u16 {
+ (u256_from_bytes(g, 2) as u16)
+ }
+
+ /// Generate a u8.
+ public fun generate_u8(g: &mut RandomGenerator): u8 {
+ (u256_from_bytes(g, 1) as u8)
+ }
+
+ /// Generate a boolean.
+ public fun generate_bool(g: &mut RandomGenerator): bool {
+ (u256_from_bytes(g, 1) & 1) == 1
+ }
+
+ // Helper function to generate a random u128 in [min, max] using a random number with num_of_bytes bytes.
+ // Assumes that the caller verified the inputs, and uses num_of_bytes to control the bias (e.g., 8 bytes larger
+ // than the actual type used by the caller function to limit the bias by 2^{-64}).
+ // TODO: Replace with a macro when we have support for it.
+ fun u128_in_range(g: &mut RandomGenerator, min: u128, max: u128, num_of_bytes: u8): u128 {
+ assert!(min <= max, EInvalidRange);
+ if (min == max) {
+ return min
+ };
+ // Pick a random number in [0, max - min] by generating a random number that is larger than max-min, and taking
+ // the modulo of the random number by the range size. Then add the min to the result to get a number in
+ // [min, max].
+ let range_size = ((max - min) as u256) + 1;
+ let rand = u256_from_bytes(g, num_of_bytes);
+ min + ((rand % range_size) as u128)
+ }
+
+ /// Generate a random u128 in [min, max] (with a bias of 2^{-64}).
+ public fun generate_u128_in_range(g: &mut RandomGenerator, min: u128, max: u128): u128 {
+ u128_in_range(g, min, max, 24)
+ }
+
+ //// Generate a random u64 in [min, max] (with a bias of 2^{-64}).
+ public fun generate_u64_in_range(g: &mut RandomGenerator, min: u64, max: u64): u64 {
+ (u128_in_range(g, (min as u128), (max as u128), 16) as u64)
+ }
+
+ /// Generate a random u32 in [min, max] (with a bias of 2^{-64}).
+ public fun generate_u32_in_range(g: &mut RandomGenerator, min: u32, max: u32): u32 {
+ (u128_in_range(g, (min as u128), (max as u128), 12) as u32)
+ }
+
+ /// Generate a random u16 in [min, max] (with a bias of 2^{-64}).
+ public fun generate_u16_in_range(g: &mut RandomGenerator, min: u16, max: u16): u16 {
+ (u128_in_range(g, (min as u128), (max as u128), 10) as u16)
+ }
+
+ /// Generate a random u8 in [min, max] (with a bias of 2^{-64}).
+ public fun generate_u8_in_range(g: &mut RandomGenerator, min: u8, max: u8): u8 {
+ (u128_in_range(g, (min as u128), (max as u128), 9) as u8)
+ }
+
+ /// Shuffle a vector using the random generator (Fisher–Yates/Knuth shuffle).
+ public fun shuffle(g: &mut RandomGenerator, v: &mut vector) {
+ let n = vector::length(v);
+ if (n == 0) {
+ return
+ };
+ assert!(n <= U16_MAX, EInvalidLength);
+ let n = (n as u16);
+ let mut i: u16 = 0;
+ let end = n - 1;
+ while (i < end) {
+ let j = generate_u16_in_range(g, i, end);
+ vector::swap(v, (i as u64), (j as u64));
+ i = i + 1;
+ };
+ }
+
+ #[test_only]
+ public fun generator_seed(r: &RandomGenerator): &vector {
+ &r.seed
+ }
+
+ #[test_only]
+ public fun generator_counter(r: &RandomGenerator): u16 {
+ r.counter
+ }
+
+ #[test_only]
+ public fun generator_buffer(r: &RandomGenerator): &vector {
+ &r.buffer
+ }
}
diff --git a/crates/sui-framework/packages/sui-framework/tests/random_tests.move b/crates/sui-framework/packages/sui-framework/tests/random_tests.move
index 62a961d56ad08..393fcda3a69bb 100644
--- a/crates/sui-framework/packages/sui-framework/tests/random_tests.move
+++ b/crates/sui-framework/packages/sui-framework/tests/random_tests.move
@@ -1,4 +1,3 @@
-
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
@@ -6,16 +5,23 @@
#[allow(unused_use)]
module sui::random_tests {
use std::vector;
-
+ use sui::test_utils::assert_eq;
+ use sui::bcs;
use sui::test_scenario;
use sui::random::{
Self,
Random,
- update_randomness_state_for_testing,
+ update_randomness_state_for_testing, new_generator, generator_seed, generator_counter, generator_buffer,
+ generate_bytes,
+ generate_u256, generate_u128, generate_u64, generate_u32, generate_u16, generate_u8, generate_u128_in_range,
+ generate_u64_in_range, generate_u32_in_range, generate_u16_in_range, generate_u8_in_range, generate_bool,
+ shuffle,
};
+ // TODO: add a test from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-22r1a.pdf ?
+
#[test]
- fun random_tests_basic() {
+ fun random_test_basic_flow() {
let mut scenario_val = test_scenario::begin(@0x0);
let scenario = &mut scenario_val;
@@ -26,11 +32,563 @@ module sui::random_tests {
update_randomness_state_for_testing(
&mut random_state,
0,
- vector[0, 1, 2, 3],
- test_scenario::ctx(scenario)
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let _o256 = generate_u256(&mut gen);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ fun test_new_generator() {
+ let global_random1 = x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F";
+ let global_random2 = x"2F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1A";
+
+ // Create Random
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::end(scenario_val);
+
+ // Set random to global_random1
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ global_random1,
+ test_scenario::ctx(scenario),
+ );
+ test_scenario::next_tx(scenario, @0x0);
+ let gen1 = new_generator(&random_state, test_scenario::ctx(scenario));
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+
+ // Set random again to global_random1
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 1,
+ global_random1,
+ test_scenario::ctx(scenario),
+ );
+ test_scenario::next_tx(scenario, @0x0);
+ let gen2 = new_generator(&random_state, test_scenario::ctx(scenario));
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+
+ // Set random to global_random2
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 2,
+ global_random2,
+ test_scenario::ctx(scenario),
+ );
+ test_scenario::next_tx(scenario, @0x0);
+ let gen3 = new_generator(&random_state, test_scenario::ctx(scenario));
+ let gen4 = new_generator(&random_state, test_scenario::ctx(scenario));
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+
+ assert!(generator_counter(&gen1) == 0, 0);
+ assert!(vector::is_empty(generator_buffer(&gen1)), 0);
+ assert!(generator_seed(&gen1) == generator_seed(&gen2), 0);
+ assert!(generator_seed(&gen1) != generator_seed(&gen3), 0);
+ assert!(generator_seed(&gen3) != generator_seed(&gen4), 0);
+ }
+
+ #[test]
+ fun random_tests_regression() {
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, @0x0);
+
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ // Regression (not critical for security, but still an indication that something is wrong).
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let o256 = generate_u256(&mut gen);
+ assert!(o256 == 85985798878417437391783029796051418802193098452099584085821130568389745847195, 0);
+ let o128 = generate_u128(&mut gen);
+ assert!(o128 == 332057125240408555349883177059479920214, 0);
+ let o64 = generate_u64(&mut gen);
+ assert!(o64 == 13202990749492462163, 0);
+ let o32 = generate_u32(&mut gen);
+ assert!(o32 == 3316307786, 0);
+ let o16 = generate_u16(&mut gen);
+ assert!(o16 == 5961, 0);
+ let o8 = generate_u8(&mut gen);
+ assert!(o8 == 222, 0);
+ let output = generate_u128_in_range(&mut gen, 51, 123456789);
+ assert!(output == 99859235, 0);
+ let output = generate_u64_in_range(&mut gen, 51, 123456789);
+ assert!(output == 87557915, 0);
+ let output = generate_u32_in_range(&mut gen, 51, 123456789);
+ assert!(output == 57096277, 0);
+ let output = generate_u16_in_range(&mut gen, 51, 1234);
+ assert!(output == 349, 0);
+ let output = generate_u8_in_range(&mut gen, 51, 123);
+ assert!(output == 60, 0);
+ let output = generate_bytes(&mut gen, 11);
+ assert!(output == x"252cfdbb59205fcc509c9e", 0);
+ let output = generate_bool(&mut gen);
+ assert!(output == true, 0);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ fun test_bytes() {
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, @0x0);
+
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+
+ // Check the output size & internal generator state
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output = generate_bytes(&mut gen, 1);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 31, 0);
+ assert!(vector::length(&output) == 1, 0);
+ let output = generate_bytes(&mut gen, 2);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 29, 0);
+ assert!(vector::length(&output) == 2, 0);
+ let output = generate_bytes(&mut gen, 29);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 0, 0);
+ assert!(vector::length(&output) == 29, 0);
+ let output = generate_bytes(&mut gen, 11);
+ assert!(generator_counter(&gen) == 2, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 21, 0);
+ assert!(vector::length(&output) == 11, 0);
+ let output = generate_bytes(&mut gen, 32 * 2);
+ assert!(generator_counter(&gen) == 4, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 21, 0);
+ assert!(vector::length(&output) == 32 * 2, 0);
+ let output = generate_bytes(&mut gen, 32 * 5 + 5);
+ assert!(generator_counter(&gen) == 9, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 16, 0);
+ assert!(vector::length(&output) == 32 * 5 + 5, 0);
+
+ // Sanity check that the output is not all zeros.
+ let output = generate_bytes(&mut gen, 10);
+ let mut i = 0;
+ loop {
+ // should break before the overflow
+ if (*vector::borrow(&output, i) != 0u8) break;
+ i = i + 1;
+ };
+
+ // Sanity check that 2 different outputs are different.
+ let output1 = generate_bytes(&mut gen, 10);
+ let output2 = generate_bytes(&mut gen, 10);
+ i = 0;
+ loop {
+ // should break before the overflow
+ if (vector::borrow(&output1, i) != vector::borrow(&output2, i)) break;
+ i = i + 1;
+ };
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ fun random_tests_uints() {
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, @0x0);
+
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ // u256
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_u256(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 0, 0);
+ let output2 = generate_u256(&mut gen);
+ assert!(generator_counter(&gen) == 2, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 0, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u8(&mut gen);
+ let _output4 = generate_u256(&mut gen);
+ assert!(generator_counter(&gen) == 4, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 31, 0);
+ // Check that we indeed generate all bytes as random
+ let mut i = 0;
+ while (i < 32) {
+ let x = generate_u256(&mut gen);
+ let x_bytes = bcs::to_bytes(&x);
+ if (*vector::borrow(&x_bytes, i) != 0u8) i = i + 1;
+ };
+
+ // u128
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_u128(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 16, 0);
+ let output2 = generate_u128(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 0, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u8(&mut gen);
+ let _output4 = generate_u128(&mut gen);
+ assert!(generator_counter(&gen) == 2, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 15, 0);
+ let mut i = 0;
+ while (i < 16) {
+ let x = generate_u128(&mut gen);
+ let x_bytes = bcs::to_bytes(&x);
+ if (*vector::borrow(&x_bytes, i) != 0u8) i = i + 1;
+ };
+
+ // u64
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_u64(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 24, 0);
+ let output2 = generate_u64(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 16, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u8(&mut gen);
+ let _output4 = generate_u64(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 7, 0);
+ let mut i = 0;
+ while (i < 8) {
+ let x = generate_u64(&mut gen);
+ let x_bytes = bcs::to_bytes(&x);
+ if (*vector::borrow(&x_bytes, i) != 0u8) i = i + 1;
+ };
+
+ // u32
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_u32(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 28, 0);
+ let output2 = generate_u32(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 24, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u8(&mut gen);
+ let _output4 = generate_u32(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 19, 0);
+ let mut i = 0;
+ while (i < 4) {
+ let x = generate_u32(&mut gen);
+ let x_bytes = bcs::to_bytes(&x);
+ if (*vector::borrow(&x_bytes, i) != 0u8) i = i + 1;
+ };
+
+ // u16
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_u16(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 30, 0);
+ let output2 = generate_u16(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 28, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u8(&mut gen);
+ let _output4 = generate_u16(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 25, 0);
+ let mut i = 0;
+ while (i < 2) {
+ let x = generate_u16(&mut gen);
+ let x_bytes = bcs::to_bytes(&x);
+ if (*vector::borrow(&x_bytes, i) != 0u8) i = i + 1;
+ };
+
+ // u8
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_u8(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 31, 0);
+ let output2 = generate_u8(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 30, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u128(&mut gen);
+ let _output4 = generate_u8(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 13, 0);
+ loop {
+ let x = generate_u8(&mut gen);
+ if (x != 0u8) break
+ };
+
+ // bool
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ assert!(vector::is_empty(generator_buffer(&gen)), 0);
+ let output1 = generate_bool(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 31, 0);
+ let output2 = generate_bool(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 30, 0);
+ assert!(output1 != output2, 0);
+ let _output3 = generate_u128(&mut gen);
+ let _output4 = generate_u8(&mut gen);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 13, 0);
+ let mut saw_false = false;
+ loop {
+ let x = generate_bool(&mut gen);
+ saw_false = saw_false || !x;
+ if (x && saw_false) break;
+ };
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ fun test_shuffle() {
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, @0x0);
+
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let mut v: vector = vector[0, 1, 2, 3, 4];
+ shuffle(&mut gen, &mut v);
+ assert!(vector::length(&v) == 5, 0);
+ let mut i: u16 = 0;
+ while (i < 5) {
+ assert!(vector::contains(&v, &i), 0);
+ i = i + 1;
+ };
+
+ // check that numbers indeed eventaually move to all positions
+ loop {
+ shuffle(&mut gen, &mut v);
+ if ((*vector::borrow(&v, 4) == 1u16)) break;
+ };
+ loop {
+ shuffle(&mut gen, &mut v);
+ if ((*vector::borrow(&v, 0) == 2u16)) break;
+ };
+
+ let mut v: vector = vector[];
+ shuffle(&mut gen, &mut v);
+ assert!(vector::length(&v) == 0, 0);
+
+ let mut v: vector = vector[321];
+ shuffle(&mut gen, &mut v);
+ assert!(vector::length(&v) == 1, 0);
+ assert!(*vector::borrow(&v, 0) == 321u32, 0);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ fun random_tests_in_range() {
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, @0x0);
+
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ // generate_u128_in_range
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let output1 = generate_u128_in_range(&mut gen, 11, 123454321);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 8, 0);
+ let output2 = generate_u128_in_range(&mut gen, 11, 123454321);
+ assert!(generator_counter(&gen) == 2, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 16, 0);
+ assert!(output1 != output2, 0);
+ let output = generate_u128_in_range(&mut gen, 123454321, 123454321 + 1);
+ assert!((output == 123454321) || (output == 123454321 + 1), 0);
+ // test the edge case of u128_in_range (covers also the other in_range functions)
+ let _output = generate_u128_in_range(&mut gen, 0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF);
+ let mut i = 0;
+ while (i < 50) {
+ let min = generate_u128(&mut gen);
+ let max = min + (generate_u64(&mut gen) as u128);
+ let output = generate_u128_in_range(&mut gen, min, max);
+ assert!(output >= min, 0);
+ assert!(output <= max, 0);
+ i = i + 1;
+ };
+
+ // generate_u64_in_range
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let output1 = generate_u64_in_range(&mut gen, 11, 123454321);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 16, 0);
+ let output2 = generate_u64_in_range(&mut gen, 11, 123454321);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 0, 0);
+ assert!(output1 != output2, 0);
+ let output = generate_u64_in_range(&mut gen, 123454321, 123454321 + 1);
+ assert!((output == 123454321) || (output == 123454321 + 1), 0);
+ let mut i = 0;
+ while (i < 50) {
+ let min = generate_u64(&mut gen);
+ let max = min + (generate_u32(&mut gen) as u64);
+ let output = generate_u64_in_range(&mut gen, min, max);
+ assert!(output >= min, 0);
+ assert!(output <= max, 0);
+ i = i + 1;
+ };
+
+ // generate_u32_in_range
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let output1 = generate_u32_in_range(&mut gen, 11, 123454321);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 20, 0);
+ let output2 = generate_u32_in_range(&mut gen, 11, 123454321);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 8, 0);
+ assert!(output1 != output2, 0);
+ let output = generate_u32_in_range(&mut gen, 123454321, 123454321 + 1);
+ assert!((output == 123454321) || (output == 123454321 + 1), 0);
+ let mut i = 0;
+ while (i < 50) {
+ let min = generate_u32(&mut gen);
+ let max = min + (generate_u16(&mut gen) as u32);
+ let output = generate_u32_in_range(&mut gen, min, max);
+ assert!(output >= min, 0);
+ assert!(output <= max, 0);
+ i = i + 1;
+ };
+
+ // generate_u16_in_range
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let output1 = generate_u16_in_range(&mut gen, 11, 12345);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 22, 0);
+ let output2 = generate_u16_in_range(&mut gen, 11, 12345);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 12, 0);
+ assert!(output1 != output2, 0);
+ let output = generate_u16_in_range(&mut gen, 12345, 12345 + 1);
+ assert!((output == 12345) || (output == 12345 + 1), 0);
+ let mut i = 0;
+ while (i < 50) {
+ let min = generate_u16(&mut gen);
+ let max = min + (generate_u8(&mut gen) as u16);
+ let output = generate_u16_in_range(&mut gen, min, max);
+ assert!(output >= min, 0);
+ assert!(output <= max, 0);
+ i = i + 1;
+ };
+
+ // generate_u8_in_range
+ gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let output1 = generate_u8_in_range(&mut gen, 11, 123);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 23, 0);
+ let output2 = generate_u8_in_range(&mut gen, 11, 123);
+ assert!(generator_counter(&gen) == 1, 0);
+ assert!(vector::length(generator_buffer(&gen)) == 14, 0);
+ assert!(output1 != output2, 0);
+ let output = generate_u8_in_range(&mut gen, 123, 123 + 1);
+ assert!((output == 123) || (output == 123 + 1), 0);
+ let mut i = 0;
+ while (i < 50) {
+ let (min, max) = (generate_u8(&mut gen), generate_u8(&mut gen));
+ let (min, max) = if (min < max) (min, max) else (max, min);
+ let (min, max) = if (min == max) (min, max + 1) else (min, max);
+ let output = generate_u8_in_range(&mut gen, min, max);
+ assert!(output >= min, 0);
+ assert!(output <= max, 0);
+ i = i + 1;
+ };
+
+ // in range with min=max should return min
+ assert_eq(generate_u32_in_range(&mut gen, 123, 123), 123);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ #[expected_failure(abort_code = random::EInvalidRange)]
+ fun random_tests_invalid_range() {
+ let mut scenario_val = test_scenario::begin(@0x0);
+ let scenario = &mut scenario_val;
+
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, @0x0);
+
+ let mut random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
);
- // TODO: Add more once user-facing API is implemented.
+ let mut gen = new_generator(&random_state, test_scenario::ctx(scenario));
+ let _output = generate_u128_in_range(&mut gen, 511, 500);
test_scenario::return_shared(random_state);
test_scenario::end(scenario_val);
@@ -49,13 +607,13 @@ module sui::random_tests {
&mut random_state,
0,
vector[0, 1, 2, 3],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
update_randomness_state_for_testing(
&mut random_state,
1,
vector[4, 5, 6, 7],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
test_scenario::next_epoch(scenario, @0x0);
@@ -64,7 +622,7 @@ module sui::random_tests {
&mut random_state,
0,
vector[8, 9, 10, 11],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
test_scenario::return_shared(random_state);
@@ -85,13 +643,13 @@ module sui::random_tests {
&mut random_state,
0,
vector[0, 1, 2, 3],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
update_randomness_state_for_testing(
&mut random_state,
0,
vector[0, 1, 2, 3],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
test_scenario::return_shared(random_state);
@@ -112,13 +670,13 @@ module sui::random_tests {
&mut random_state,
0,
vector[0, 1, 2, 3],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
update_randomness_state_for_testing(
&mut random_state,
3,
vector[0, 1, 2, 3],
- test_scenario::ctx(scenario)
+ test_scenario::ctx(scenario),
);
test_scenario::return_shared(random_state);
diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap
index 8a060d534aa15..a2c951bfcd8ae 100644
--- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap
+++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap
@@ -240,13 +240,13 @@ validators:
next_epoch_worker_address: ~
extra_fields:
id:
- id: "0xbd30c27f7e1bba23be03029496b0153427b2a6b62342e2144ddb3f7e3b3bfcac"
+ id: "0x9ba102555aefff34898f1240c70527f528200003ae841cb57cd7d40a7f9a986e"
size: 0
voting_power: 10000
- operation_cap_id: "0xccf67fe04c6a4cca7de3f13504b00662228596ada99e17c1e9e19996b86964f6"
+ operation_cap_id: "0xc2a525d6e84f9f9082e2a50f6f6e19e1cef582c49ef9e9952d857c2a52c95615"
gas_price: 1000
staking_pool:
- id: "0x5b1db67d8a069bacc26bd993979f173fbb3a0ce6d8ae90c2a48cae492b06b25c"
+ id: "0xbaa5dc0e24e9a7f976b79b04353192d3c126b10ea60a89d31c8a50a847eda95c"
activation_epoch: 0
deactivation_epoch: ~
sui_balance: 20000000000000000
@@ -254,14 +254,14 @@ validators:
value: 0
pool_token_balance: 20000000000000000
exchange_rates:
- id: "0x24a627d9d21be7bb10d0b4508c74337db3bef1c862b7306c8c5b85b100a6138a"
+ id: "0xb402891e5af76ca819b2c8782b887b94770f86727a626c067b7bf99cd78d7528"
size: 1
pending_stake: 0
pending_total_sui_withdraw: 0
pending_pool_token_withdraw: 0
extra_fields:
id:
- id: "0x66b1868391655b9471481dc429d89eca0f088ad2b702a5459af3049f5371a7cf"
+ id: "0xf4380b30a25b68991cffec25f805b2202d956aff5485fcf70fb14e6e41935130"
size: 0
commission_rate: 200
next_epoch_stake: 20000000000000000
@@ -269,27 +269,27 @@ validators:
next_epoch_commission_rate: 200
extra_fields:
id:
- id: "0xded819d256bbd3352632e83c09c20c21a61c9e7b42135092b5212c0b61b3b19d"
+ id: "0x1f623545b36c41227e7352235dc78836d2287e3ab652d4d6bab41d6bff830637"
size: 0
pending_active_validators:
contents:
- id: "0x82a3a8c972ff446b20086ae5b55f37ac2a2781ec1042fb3f33b0eec729df01c4"
+ id: "0x731c6458e1e53b261efd1a198d9be78c04285c050daa9822129e09a10d762d5c"
size: 0
pending_removals: []
staking_pool_mappings:
- id: "0x2440bf3ff18377a98b6b0594b41393c921c5ae83e3807724bda348c74c433d22"
+ id: "0xe8f3d3aa0861f6f6d11678896d53090e08efe24a21a5b477e64884cf8396c150"
size: 1
inactive_validators:
- id: "0xa0eeb75a32a4a97d7acf1363df47fc1e1cd9252884d75026e4465e9cd541bd88"
+ id: "0xde531fb6ff92e3b5791614931e7742b63688706f2070d2829ba3f1e96ea6d907"
size: 0
validator_candidates:
- id: "0xe594528b39b4adf84cd091779c14dc8383d28fcee242fa518de7080e31dc5409"
+ id: "0xc4556750e835f2b1813e322eec5ec85ddcb9b4612c424f92c2a8d9c620004568"
size: 0
at_risk_validators:
contents: []
extra_fields:
id:
- id: "0x8dcf49f8aba31b5e5600f85766453ee45b4407ca326694dd8ed089a6c063c371"
+ id: "0x24d3a110b3a2960b18cf12ee4529bd1211e80732a5289c3777257287f4128d94"
size: 0
storage_fund:
total_object_storage_rebates:
@@ -306,7 +306,7 @@ parameters:
validator_low_stake_grace_period: 7
extra_fields:
id:
- id: "0x87d00e604247bbebfb8fa93c76d871feaa0b3aa716526b9d96444ea1c361a8d1"
+ id: "0x292f323ab679f1a1d9674963bdaeda9a16bfab6cd86314bc95a7f2e618d5aaed"
size: 0
reference_gas_price: 1000
validator_report_records:
@@ -320,7 +320,7 @@ stake_subsidy:
stake_subsidy_decrease_rate: 1000
extra_fields:
id:
- id: "0xb1805990a7888ccf1cad230790b57a778689de0b44268c72e13abaddbf280dcb"
+ id: "0x9016457565abef8804500f298fa8b570fc2703b1e48962a73ebf2c2327b371df"
size: 0
safe_mode: false
safe_mode_storage_rewards:
@@ -332,6 +332,6 @@ safe_mode_non_refundable_storage_fee: 0
epoch_start_timestamp_ms: 10
extra_fields:
id:
- id: "0xe77af8cb9a1b07cb495fc3c9991226e5e5e620b7878e670cb20362c20df0d498"
+ id: "0xdb4cb00356f5b5e878275141fe6bdfe984633103f766bf60d7f419873581bd86"
size: 0
diff --git a/sui_programmability/examples/games/sources/raffles.move b/sui_programmability/examples/games/sources/raffles.move
new file mode 100644
index 0000000000000..630f5684f7528
--- /dev/null
+++ b/sui_programmability/examples/games/sources/raffles.move
@@ -0,0 +1,233 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+/// Basic raffles games that depends on Sui randomness.
+///
+/// Anyone can create a new raffle game with an end time and a price. After the end time, anyone can trigger
+/// a function to determine the winner, and the winner gets the entire balance of the game.
+///
+/// - raffle_with_tickets uses tickets which could be transferred to other accounts, used as NFTs, etc.
+/// - small_raffle uses a simpler approach with no tickets.
+
+module games::raffle_with_tickets {
+ use std::option::{Self, Option};
+ use sui::balance::{Self, Balance};
+ use sui::clock::{Self, Clock};
+ use sui::coin::{Self, Coin};
+ use sui::object::{Self, ID, UID};
+ use sui::random::{Self, Random, new_generator};
+ use sui::sui::SUI;
+ use sui::transfer;
+ use sui::tx_context::TxContext;
+
+ /// Error codes
+ const EGameInProgress: u64 = 0;
+ const EGameAlreadyCompleted: u64 = 1;
+ const EInvalidAmount: u64 = 2;
+ const EGameMismatch: u64 = 3;
+ const ENotWinner: u64 = 4;
+ const ENoParticipants: u64 = 4;
+
+ /// Game represents a set of parameters of a single game.
+ struct Game has key {
+ id: UID,
+ cost_in_sui: u64,
+ participants: u32,
+ end_time: u64,
+ winner: Option,
+ balance: Balance,
+ }
+
+ /// Ticket represents a participant in a single game.
+ struct Ticket has key {
+ id: UID,
+ game_id: ID,
+ participant_index: u32,
+ }
+
+ /// Create a shared-object Game.
+ public fun create(end_time: u64, cost_in_sui: u64, ctx: &mut TxContext) {
+ let game = Game {
+ id: object::new(ctx),
+ cost_in_sui,
+ participants: 0,
+ end_time,
+ winner: option::none(),
+ balance: balance::zero(),
+ };
+ transfer::share_object(game);
+ }
+
+ /// Anyone can determine a winner.
+ ///
+ /// The function is defined as private entry to prevent calls from other Move functions. (If calls from other
+ /// functions are allowed, the calling function might abort the transaction depending on the winner.)
+ /// Gas based attacks are not possible since the gas cost of this function is independent of the winner.
+ entry fun determine_winner(game: &mut Game, r: &Random, clock: &Clock, ctx: &mut TxContext) {
+ assert!(game.end_time <= clock::timestamp_ms(clock), EGameInProgress);
+ assert!(option::is_none(&game.winner), EGameAlreadyCompleted);
+ assert!(game.participants > 0, ENoParticipants);
+ let generator = new_generator(r, ctx);
+ let winner = random::generate_u32_in_range(&mut generator, 1, game.participants);
+ game.winner = option::some(winner);
+ }
+
+ /// Anyone can play and receive a ticket.
+ public fun buy_ticket(game: &mut Game, coin: Coin, clock: &Clock, ctx: &mut TxContext): Ticket {
+ assert!(game.end_time > clock::timestamp_ms(clock), EGameAlreadyCompleted);
+ assert!(coin::value(&coin) == game.cost_in_sui, EInvalidAmount);
+
+ game.participants = game.participants + 1;
+ coin::put(&mut game.balance, coin);
+
+ Ticket {
+ id: object::new(ctx),
+ game_id: object::id(game),
+ participant_index: game.participants,
+ }
+ }
+
+ /// The winner can take the prize.
+ public fun redeem(ticket: Ticket, game: Game, ctx: &mut TxContext): Coin {
+ assert!(object::id(&game) == ticket.game_id, EGameMismatch);
+ assert!(option::contains(&game.winner, &ticket.participant_index), ENotWinner);
+ destroy_ticket(ticket);
+
+ let Game { id, cost_in_sui: _, participants: _, end_time: _, winner: _, balance } = game;
+ object::delete(id);
+ let reward = coin::from_balance(balance, ctx);
+ reward
+ }
+
+ public fun destroy_ticket(ticket: Ticket) {
+ let Ticket { id, game_id: _, participant_index: _ } = ticket;
+ object::delete(id);
+ }
+
+ #[test_only]
+ public fun get_cost_in_sui(game: &Game): u64 {
+ game.cost_in_sui
+ }
+
+ #[test_only]
+ public fun get_end_time(game: &Game): u64 {
+ game.end_time
+ }
+
+ #[test_only]
+ public fun get_participants(game: &Game): u32 {
+ game.participants
+ }
+
+ #[test_only]
+ public fun get_winner(game: &Game): Option {
+ game.winner
+ }
+
+ #[test_only]
+ public fun get_balance(game: &Game): u64 {
+ balance::value(&game.balance)
+ }
+}
+
+
+module games::small_raffle {
+ use sui::balance::{Self, Balance};
+ use sui::clock::{Self, Clock};
+ use sui::coin::{Self, Coin};
+ use sui::object::{Self, UID};
+ use sui::random::{Self, Random, new_generator};
+ use sui::sui::SUI;
+ use sui::table::{Self, Table};
+ use sui::transfer;
+ use sui::tx_context::{TxContext, sender};
+
+ /// Error codes
+ const EGameInProgress: u64 = 0;
+ const EGameAlreadyCompleted: u64 = 1;
+ const EInvalidAmount: u64 = 2;
+ const EReachedMaxParticipants: u64 = 3;
+
+ const MaxParticipants: u32 = 500;
+
+ /// Game represents a set of parameters of a single game.
+ struct Game has key {
+ id: UID,
+ cost_in_sui: u64,
+ participants: u32,
+ end_time: u64,
+ balance: Balance,
+ participants_table: Table,
+ }
+
+ /// Create a shared-object Game.
+ public fun create(end_time: u64, cost_in_sui: u64, ctx: &mut TxContext) {
+ let game = Game {
+ id: object::new(ctx),
+ cost_in_sui,
+ participants: 0,
+ end_time,
+ balance: balance::zero(),
+ participants_table: table::new(ctx),
+ };
+ transfer::share_object(game);
+ }
+
+ /// Anyone can close the game and send the balance to the winner.
+ ///
+ /// The function is defined as private entry to prevent calls from other Move functions. (If calls from other
+ /// functions are allowed, the calling function might abort the transaction depending on the winner.)
+ /// Gas based attacks are not possible since the gas cost of this function is independent of the winner.
+ entry fun close(game: Game, r: &Random, clock: &Clock, ctx: &mut TxContext) {
+ assert!(game.end_time <= clock::timestamp_ms(clock), EGameInProgress);
+ let Game { id, cost_in_sui: _, participants, end_time: _, balance, participants_table } = game;
+ if (participants > 0) {
+ let generator = new_generator(r, ctx);
+ let winner = random::generate_u32_in_range(&mut generator, 1, participants);
+ let winner_address = *table::borrow(&participants_table, winner);
+ let reward = coin::from_balance(balance, ctx);
+ transfer::public_transfer(reward, winner_address);
+ } else {
+ balance::destroy_zero(balance);
+ };
+
+ let i = 1;
+ while (i <= participants) {
+ table::remove(&mut participants_table, i);
+ i = i + 1;
+ };
+ table::destroy_empty(participants_table);
+ object::delete(id);
+ }
+
+ /// Anyone can play.
+ public fun play(game: &mut Game, coin: Coin, clock: &Clock, ctx: &mut TxContext) {
+ assert!(game.end_time > clock::timestamp_ms(clock), EGameAlreadyCompleted);
+ assert!(coin::value(&coin) == game.cost_in_sui, EInvalidAmount);
+ assert!(game.participants < MaxParticipants, EReachedMaxParticipants);
+
+ game.participants = game.participants + 1;
+ coin::put(&mut game.balance, coin);
+ table::add(&mut game.participants_table, game.participants, sender(ctx));
+ }
+
+ #[test_only]
+ public fun get_cost_in_sui(game: &Game): u64 {
+ game.cost_in_sui
+ }
+
+ #[test_only]
+ public fun get_end_time(game: &Game): u64 {
+ game.end_time
+ }
+
+ #[test_only]
+ public fun get_participants(game: &Game): u32 {
+ game.participants
+ }
+
+ #[test_only]
+ public fun get_balance(game: &Game): u64 {
+ balance::value(&game.balance)
+ }
+}
diff --git a/sui_programmability/examples/games/sources/slot_machine.move b/sui_programmability/examples/games/sources/slot_machine.move
new file mode 100644
index 0000000000000..774dd24141222
--- /dev/null
+++ b/sui_programmability/examples/games/sources/slot_machine.move
@@ -0,0 +1,92 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+/// A betting game that depends on Sui randomness.
+///
+/// Anyone can create a new game for the current epoch by depositing SUI as the initial balance. The creator can
+/// withdraw the remaining balance after the epoch is over.
+///
+/// Anyone can play the game by betting on X SUI. They win X with probability 49% and lose the X SUI otherwise.
+///
+module games::slot_machine {
+ use sui::balance::{Self, Balance};
+ use sui::coin::{Self, Coin};
+ use sui::math;
+ use sui::object::{Self, UID};
+ use sui::random::{Self, Random, new_generator};
+ use sui::sui::SUI;
+ use sui::transfer;
+ use sui::tx_context::{Self, TxContext};
+
+ /// Error codes
+ const EInvalidAmount: u64 = 0;
+ const EInvalidSender: u64 = 1;
+ const EInvalidEpoch: u64 = 2;
+
+ /// Game for a specific epoch.
+ struct Game has key {
+ id: UID,
+ creator: address,
+ epoch: u64,
+ balance: Balance,
+ }
+
+ /// Create a new game with a given initial reward for the current epoch.
+ public fun create(
+ reward: Coin,
+ ctx: &mut TxContext,
+ ) {
+ let amount = coin::value(&reward);
+ assert!(amount > 0, EInvalidAmount);
+ transfer::share_object(Game {
+ id: object::new(ctx),
+ creator: tx_context::sender(ctx),
+ epoch: tx_context::epoch(ctx),
+ balance: coin::into_balance(reward),
+ });
+ }
+
+ /// Creator can withdraw remaining balance if the game is over.
+ public fun close(game: Game, ctx: &mut TxContext): Coin {
+ assert!(tx_context::epoch(ctx) > game.epoch, EInvalidEpoch);
+ assert!(tx_context::sender(ctx) == game.creator, EInvalidSender);
+ let Game { id, creator: _, epoch: _, balance } = game;
+ object::delete(id);
+ coin::from_balance(balance, ctx)
+ }
+
+ /// Play one turn of the game.
+ ///
+ /// The function consumes the same amount of gas independently of the random outcome.
+ entry fun play(game: &mut Game, r: &Random, coin: &mut Coin, ctx: &mut TxContext) {
+ assert!(tx_context::epoch(ctx) == game.epoch, EInvalidEpoch);
+ assert!(coin::value(coin) > 0, EInvalidAmount);
+
+ // play the game
+ let generator = new_generator(r, ctx);
+ let bet = random::generate_u8_in_range(&mut generator, 1, 100);
+ let lost = bet / 50; // 0 with probability 49%, and 1 or 2 with probability 51%
+ let won = (2 - lost) / 2; // 1 with probability 49%, and 0 with probability 51%
+
+ // move the bet amount from the user's coin to the game's balance
+ let coin_value = coin::value(coin);
+ let bet_amount = math::min(coin_value, balance::value(&game.balance));
+ coin::put(&mut game.balance, coin::split(coin, bet_amount, ctx));
+
+ // move the reward to the user's coin
+ let reward = 2 * (won as u64) * bet_amount;
+ // the assumption here is that the next line does not consumes more gas when called with zero reward than with
+ // non-zero reward
+ coin::join(coin, coin::take(&mut game.balance, reward, ctx));
+ }
+
+ #[test_only]
+ public fun get_balance(game: &Game): u64 {
+ balance::value(&game.balance)
+ }
+
+ #[test_only]
+ public fun get_epoch(game: &Game): u64 {
+ game.epoch
+ }
+}
diff --git a/sui_programmability/examples/games/tests/raffles_tests.move b/sui_programmability/examples/games/tests/raffles_tests.move
new file mode 100644
index 0000000000000..60a84d32eb089
--- /dev/null
+++ b/sui_programmability/examples/games/tests/raffles_tests.move
@@ -0,0 +1,177 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+#[test_only]
+module games::raffles_tests {
+ use std::option;
+ use sui::clock;
+ use sui::coin::{Self, Coin};
+ use sui::random::{Self, update_randomness_state_for_testing, Random};
+ use sui::sui::SUI;
+ use sui::test_scenario::{Self, Scenario};
+ use sui::transfer;
+ use games::small_raffle;
+
+ use games::raffle_with_tickets;
+
+ fun mint(addr: address, amount: u64, scenario: &mut Scenario) {
+ transfer::public_transfer(coin::mint_for_testing(amount, test_scenario::ctx(scenario)), addr);
+ test_scenario::next_tx(scenario, addr);
+ }
+
+ #[test]
+ fun test_game_with_tickets() {
+ let user1 = @0x0;
+ let user2 = @0x1;
+ let user3 = @0x2;
+ let user4 = @0x3;
+
+ let scenario_val = test_scenario::begin(user1);
+ let scenario = &mut scenario_val;
+
+ // Setup randomness
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ // Create the game and get back the output objects.
+ mint(user1, 1000, scenario);
+ let end_time = 100;
+ raffle_with_tickets::create(end_time, 10, test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let game = test_scenario::take_shared(scenario);
+ assert!(raffle_with_tickets::get_cost_in_sui(&game) == 10, 1);
+ assert!(raffle_with_tickets::get_participants(&game) == 0, 1);
+ assert!(raffle_with_tickets::get_end_time(&game) == end_time, 1);
+ assert!(raffle_with_tickets::get_winner(&game) == option::none(), 1);
+ assert!(raffle_with_tickets::get_balance(&game) == 0, 1);
+
+ let clock = clock::create_for_testing(test_scenario::ctx(scenario));
+ clock::set_for_testing(&mut clock, 10);
+
+ // Play with 4 users (everything here is deterministic)
+ test_scenario::next_tx(scenario, user1);
+ mint(user1, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ let t1 = raffle_with_tickets::buy_ticket(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(raffle_with_tickets::get_participants(&game) == 1, 1);
+ raffle_with_tickets::destroy_ticket(t1); // loser
+
+ test_scenario::next_tx(scenario, user2);
+ mint(user2, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ let t2 = raffle_with_tickets::buy_ticket(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(raffle_with_tickets::get_participants(&game) == 2, 1);
+ raffle_with_tickets::destroy_ticket(t2); // loser
+
+ test_scenario::next_tx(scenario, user3);
+ mint(user3, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ let t3 = raffle_with_tickets::buy_ticket(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(raffle_with_tickets::get_participants(&game) == 3, 1);
+ raffle_with_tickets::destroy_ticket(t3); // loser
+
+ test_scenario::next_tx(scenario, user4);
+ mint(user4, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ let t4 = raffle_with_tickets::buy_ticket(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(raffle_with_tickets::get_participants(&game) == 4, 1);
+ // this is the winner
+
+ // Determine the winner (-> user3)
+ clock::set_for_testing(&mut clock, 101);
+ raffle_with_tickets::determine_winner(&mut game, &random_state, &clock, test_scenario::ctx(scenario));
+ assert!(raffle_with_tickets::get_winner(&game) == option::some(4), 1);
+ assert!(raffle_with_tickets::get_balance(&game) == 40, 1);
+ clock::destroy_for_testing(clock);
+
+ // Take the reward
+ let coin = raffle_with_tickets::redeem(t4, game, test_scenario::ctx(scenario));
+ assert!(coin::value(&coin) == 40, 1);
+ coin::burn_for_testing(coin);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+
+ #[test]
+ fun test_small_raffle() {
+ let user1 = @0x0;
+ let user2 = @0x1;
+ let user3 = @0x2;
+ let user4 = @0x3;
+
+ let scenario_val = test_scenario::begin(user1);
+ let scenario = &mut scenario_val;
+
+ // Setup randomness
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ // Create the game and get back the output objects.
+ mint(user1, 1000, scenario);
+ let end_time = 100;
+ small_raffle::create(end_time, 10, test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let game = test_scenario::take_shared(scenario);
+ assert!(small_raffle::get_cost_in_sui(&game) == 10, 1);
+ assert!(small_raffle::get_participants(&game) == 0, 1);
+ assert!(small_raffle::get_end_time(&game) == end_time, 1);
+ assert!(small_raffle::get_balance(&game) == 0, 1);
+
+ let clock = clock::create_for_testing(test_scenario::ctx(scenario));
+ clock::set_for_testing(&mut clock, 10);
+
+ // Play with 4 users (everything here is deterministic)
+ test_scenario::next_tx(scenario, user1);
+ mint(user1, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ small_raffle::play(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(small_raffle::get_participants(&game) == 1, 1);
+
+ test_scenario::next_tx(scenario, user2);
+ mint(user2, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ small_raffle::play(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(small_raffle::get_participants(&game) == 2, 1);
+
+ test_scenario::next_tx(scenario, user3);
+ mint(user3, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ small_raffle::play(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(small_raffle::get_participants(&game) == 3, 1);
+
+ test_scenario::next_tx(scenario, user4);
+ mint(user4, 10, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ small_raffle::play(&mut game, coin, &clock, test_scenario::ctx(scenario));
+ assert!(small_raffle::get_participants(&game) == 4, 1);
+
+ // Determine the winner (-> user4)
+ clock::set_for_testing(&mut clock, 101);
+ small_raffle::close(game, &random_state, &clock, test_scenario::ctx(scenario));
+ clock::destroy_for_testing(clock);
+
+ // Check that received the reward
+ test_scenario::next_tx(scenario, user4);
+ let coin = test_scenario::take_from_sender>(scenario);
+ assert!(coin::value(&coin) == 40, 1);
+ coin::burn_for_testing(coin);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+}
diff --git a/sui_programmability/examples/games/tests/slot_machine_tests.move b/sui_programmability/examples/games/tests/slot_machine_tests.move
new file mode 100644
index 0000000000000..9d14c5bc6c78f
--- /dev/null
+++ b/sui_programmability/examples/games/tests/slot_machine_tests.move
@@ -0,0 +1,92 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+#[test_only]
+module games::slot_machine_tests {
+ use sui::coin::{Self, Coin};
+ use sui::random::{Self, update_randomness_state_for_testing, Random};
+ use sui::sui::SUI;
+ use sui::test_scenario::{Self, Scenario};
+ use sui::transfer;
+ use games::slot_machine;
+
+ fun mint(addr: address, amount: u64, scenario: &mut Scenario) {
+ transfer::public_transfer(coin::mint_for_testing(amount, test_scenario::ctx(scenario)), addr);
+ test_scenario::next_tx(scenario, addr);
+ }
+
+ #[test]
+ fun test_game() {
+ let user1 = @0x0;
+ let user2 = @0x1;
+ let scenario_val = test_scenario::begin(user1);
+ let scenario = &mut scenario_val;
+
+ // Setup randomness
+ random::create_for_testing(test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ // Create the game and get back the output objects.
+ mint(user1, 1000, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ slot_machine::create(coin, test_scenario::ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let game = test_scenario::take_shared(scenario);
+ assert!(slot_machine::get_balance(&game) == 1000, 1);
+ assert!(slot_machine::get_epoch(&game) == 0, 1);
+
+ // Play 4 turns (everything here is deterministic)
+ test_scenario::next_tx(scenario, user2);
+ mint(user2, 100, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ slot_machine::play(&mut game, &random_state, &mut coin, test_scenario::ctx(scenario));
+ assert!(slot_machine::get_balance(&game) == 1100, 1); // lost 100
+ assert!(coin::value(&coin) == 0, 1);
+ test_scenario::return_to_sender(scenario, coin);
+
+ test_scenario::next_tx(scenario, user2);
+ mint(user2, 200, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ slot_machine::play(&mut game, &random_state, &mut coin, test_scenario::ctx(scenario));
+ assert!(slot_machine::get_balance(&game) == 900, 1); // won 200
+ // check that received the right amount
+ assert!(coin::value(&coin) == 400, 1);
+ test_scenario::return_to_sender(scenario, coin);
+
+ test_scenario::next_tx(scenario, user2);
+ mint(user2, 300, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ slot_machine::play(&mut game, &random_state, &mut coin, test_scenario::ctx(scenario));
+ assert!(slot_machine::get_balance(&game) == 600, 1); // won 300
+ // check that received the remaining amount
+ assert!(coin::value(&coin) == 600, 1);
+ test_scenario::return_to_sender(scenario, coin);
+
+ test_scenario::next_tx(scenario, user2);
+ mint(user2, 200, scenario);
+ let coin = test_scenario::take_from_sender>(scenario);
+ slot_machine::play(&mut game, &random_state, &mut coin, test_scenario::ctx(scenario));
+ assert!(slot_machine::get_balance(&game) == 800, 1); // lost 200
+ // check that received the right amount
+ assert!(coin::value(&coin) == 0, 1);
+ test_scenario::return_to_sender(scenario, coin);
+
+ // TODO: test also that the last coin is taken
+
+ // Take remaining balance
+ test_scenario::next_epoch(scenario, user1);
+ let coin = slot_machine::close(game, test_scenario::ctx(scenario));
+ assert!(coin::value(&coin) == 800, 1);
+ coin::burn_for_testing(coin);
+
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+}
diff --git a/sui_programmability/examples/nfts/sources/random_nft.move b/sui_programmability/examples/nfts/sources/random_nft.move
new file mode 100644
index 0000000000000..d8146762fac9a
--- /dev/null
+++ b/sui_programmability/examples/nfts/sources/random_nft.move
@@ -0,0 +1,230 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+/// A simple NFT that can be airdropped to users without a value and converted to a random metal NFT.
+/// The probability of getting a gold, silver, or bronze NFT is 10%, 30%, and 60% respectively.
+module nfts::random_nft_airdrop {
+ use std::string;
+ use std::vector;
+ use sui::object::{Self, UID, delete};
+ use sui::random;
+ use sui::random::{Random, new_generator};
+ use sui::transfer;
+ use sui::tx_context::{Self, TxContext};
+
+ const EInvalidParams: u64 = 0;
+
+ const GOLD: u8 = 1;
+ const SILVER: u8 = 2;
+ const BRONZE: u8 = 3;
+
+ struct AirDropNFT has key, store {
+ id: UID,
+ }
+
+ struct MetalNFT has key, store {
+ id: UID,
+ metal: u8,
+ }
+
+ struct MintingCapability has key {
+ id: UID,
+ }
+
+ #[allow(unused_function)]
+ fun init(ctx: &mut TxContext) {
+ transfer::transfer(
+ MintingCapability { id: object::new(ctx) },
+ tx_context::sender(ctx),
+ );
+ }
+
+ public fun mint(_cap: &MintingCapability, n: u16, ctx: &mut TxContext): vector {
+ let result = vector[];
+ let i = 0;
+ while (i < n) {
+ vector::push_back(&mut result, AirDropNFT { id: object::new(ctx) });
+ i = i + 1;
+ };
+ result
+ }
+
+
+ /// Reveal the metal of the airdrop NFT and convert it to a metal NFT.
+ /// This function uses arithmetic_is_less_than to determine the metal of the NFT in a way that consumes the same
+ /// amount of gas regardless of the value of the random number.
+ /// See reveal_alternative1 and reveal_alternative2_step1 for different implementations.
+ entry fun reveal(nft: AirDropNFT, r: &Random, ctx: &mut TxContext) {
+ destroy_airdrop_nft(nft);
+
+ let generator = new_generator(r, ctx);
+ let v = random::generate_u8_in_range(&mut generator, 1, 100);
+
+ let is_gold = arithmetic_is_less_than(v, 11, 100); // probability of 10%
+ let is_silver = arithmetic_is_less_than(v, 41, 100) * (1 - is_gold); // probability of 30%
+ let is_bronze = (1 - is_gold) * (1 - is_silver); // probability of 60%
+ let metal = is_gold * GOLD + is_silver * SILVER + is_bronze * BRONZE;
+
+ transfer::public_transfer(
+ MetalNFT { id: object::new(ctx), metal, },
+ tx_context::sender(ctx)
+ );
+ }
+
+ // Implements "is v < w? where v <= v_max" using integer arithmetic. Returns 1 if true, 0 otherwise.
+ // Safe in case w and v_max are independent of the randomenss (e.g., fixed).
+ // Does not check if v <= v_max.
+ fun arithmetic_is_less_than(v: u8, w: u8, v_max: u8): u8 {
+ assert!(v_max >= w && w > 0, EInvalidParams);
+ let v_max_over_w = v_max / w;
+ let v_over_w = v / w; // 0 if v < w, [1, v_max_over_w] if above
+ (v_max_over_w - v_over_w) / v_max_over_w
+ }
+
+
+ /// An alternative implementation of reveal that uses if-else statements to determine the metal of the NFT.
+ /// Here the "happier flows" consume more gas than the less happy ones (it assumes that users always prefer the
+ /// rarest metals).
+ entry fun reveal_alternative1(nft: AirDropNFT, r: &Random, ctx: &mut TxContext) {
+ destroy_airdrop_nft(nft);
+
+ let generator = new_generator(r, ctx);
+ let v = random::generate_u8_in_range(&mut generator, 1, 100);
+
+ if (v <= 60) {
+ transfer::public_transfer(
+ MetalNFT { id: object::new(ctx), metal: BRONZE, },
+ tx_context::sender(ctx),
+ );
+ } else if (v <= 90) {
+ transfer::public_transfer(
+ MetalNFT { id: object::new(ctx), metal: SILVER, },
+ tx_context::sender(ctx),
+ );
+ } else if (v <= 100) {
+ transfer::public_transfer(
+ MetalNFT { id: object::new(ctx), metal: GOLD, },
+ tx_context::sender(ctx),
+ );
+ };
+ }
+
+
+ /// An alternative implementation of reveal that uses two steps to determine the metal of the NFT.
+ /// reveal_alternative2_step1 retrieves the random value, and reveal_alternative2_step2 determines the metal.
+
+ struct RandomnessNFT has key, store {
+ id: UID,
+ value: u8,
+ }
+
+ entry fun reveal_alternative2_step1(nft: AirDropNFT, r: &Random, ctx: &mut TxContext) {
+ destroy_airdrop_nft(nft);
+
+ let generator = new_generator(r, ctx);
+ let v = random::generate_u8_in_range(&mut generator, 1, 100);
+
+ transfer::public_transfer(
+ RandomnessNFT { id: object::new(ctx), value: v, },
+ tx_context::sender(ctx),
+ );
+ }
+
+ public fun reveal_alternative2_step2(nft: RandomnessNFT, ctx: &mut TxContext): MetalNFT {
+ let RandomnessNFT { id, value } = nft;
+ delete(id);
+
+ let metal =
+ if (value <= 10) GOLD
+ else if (10 < value && value <= 40) SILVER
+ else BRONZE;
+
+ MetalNFT {
+ id: object::new(ctx),
+ metal,
+ }
+ }
+
+ fun destroy_airdrop_nft(nft: AirDropNFT) {
+ let AirDropNFT { id } = nft;
+ object::delete(id)
+ }
+
+ public fun metal_string(nft: &MetalNFT): string::String {
+ if (nft.metal == GOLD) string::utf8(b"Gold")
+ else if (nft.metal == SILVER) string::utf8(b"Silver")
+ else string::utf8(b"Bronze")
+ }
+
+ #[test_only]
+ public fun destroy_cap(cap: MintingCapability) {
+ let MintingCapability { id } = cap;
+ object::delete(id)
+ }
+
+ #[test_only]
+ public fun test_init(ctx: &mut TxContext) {
+ init(ctx)
+ }
+}
+
+#[test_only]
+module nfts::random_nft_airdrop_tests {
+ use sui::test_scenario;
+ use std::string;
+ use std::vector;
+ use sui::random;
+ use sui::random::{Random, update_randomness_state_for_testing};
+ use sui::test_scenario::{ctx, take_from_sender, next_tx, return_to_sender};
+ use nfts::random_nft_airdrop::{Self, MintingCapability, MetalNFT};
+
+ #[test]
+ fun test_e2e() {
+ let user0 = @0x0;
+ let user1 = @0x1;
+ let scenario_val = test_scenario::begin(user0);
+ let scenario = &mut scenario_val;
+
+ // Setup randomness
+ random::create_for_testing(ctx(scenario));
+ test_scenario::next_tx(scenario, user0);
+ let random_state = test_scenario::take_shared(scenario);
+ update_randomness_state_for_testing(
+ &mut random_state,
+ 0,
+ x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
+ test_scenario::ctx(scenario),
+ );
+
+ test_scenario::next_tx(scenario, user1);
+ // mint airdrops
+ random_nft_airdrop::test_init(ctx(scenario));
+ test_scenario::next_tx(scenario, user1);
+ let cap = take_from_sender(scenario);
+ let nfts = random_nft_airdrop::mint(&cap, 20, ctx(scenario));
+
+ let seen_gold = false;
+ let seen_silver = false;
+ let seen_bronze = false;
+ let i = 0;
+ while (i < 20) {
+ if (i % 2 == 1) random_nft_airdrop::reveal(vector::pop_back(&mut nfts), &random_state, ctx(scenario))
+ else random_nft_airdrop::reveal_alternative1(vector::pop_back(&mut nfts), &random_state, ctx(scenario));
+ next_tx(scenario, user1);
+ let nft = take_from_sender(scenario);
+ let metal = random_nft_airdrop::metal_string(&nft);
+ seen_gold = seen_gold || metal == string::utf8(b"Gold");
+ seen_silver = seen_silver || metal == string::utf8(b"Silver");
+ seen_bronze = seen_bronze || metal == string::utf8(b"Bronze");
+ return_to_sender(scenario, nft);
+ i = i + 1;
+ };
+
+ assert!(seen_gold && seen_silver && seen_bronze, 1);
+
+ vector::destroy_empty(nfts);
+ random_nft_airdrop::destroy_cap(cap);
+ test_scenario::return_shared(random_state);
+ test_scenario::end(scenario_val);
+ }
+}