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); + } +}