Skip to content

Commit

Permalink
Use drand quicknet in examples (#14926)
Browse files Browse the repository at this point in the history
## Description 

Use DRAND's quicknet in example games. Quicknet has a period of 3
seconds instead of the default chains 30 seconds.

## Test Plan 

Unit tests.

### Release notes

Update DRAND lib to use the new quicknet network which has a 3 second
period.
  • Loading branch information
jonas-lj authored Nov 28, 2023
1 parent 2ee011c commit ef5b712
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 59 deletions.
2 changes: 2 additions & 0 deletions sui_programmability/examples/games/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ Examples of toy games built on top of Sui!
* SeaHero: a permissionless mod of the Hero game where the hero can slay sea monsters to earn RUM tokens.
* SeaHeroHelper: a permissionless mod of the economics of the Sea Hero game. A weak hero can request help from a stronger hero, who receives a share of the monster slaying reward.
* RockPaperScissors: a commit-reveal scheme in which players first submit their commitments and then reveal the data that led to these commitments.
* DrandBasedLottery: a lottery game that depends on randomness from drand
* DrandBasedScratchCard: a scratch card game that depends on randomness from drand
24 changes: 12 additions & 12 deletions sui_programmability/examples/games/sources/drand_based_lottery.move
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/// A basic game that depends on randomness from drand (chained mode).
/// A basic game that depends on randomness from drand.
///
/// The main chain of drand creates random 32 bytes every 30 seconds. This randomness is verifiable in the sense
/// The quicknet chain chain of drand creates random 32 bytes every 3 seconds. This randomness is verifiable in the sense
/// that anyone can check if a given 32 bytes bytes are indeed the i-th output of drand. For more details see
/// https://drand.love/
///
Expand All @@ -17,17 +17,17 @@
/// - The game is defined for a specific drand round N in the future, for example, the round that is expected in
/// 5 mins from now.
/// The current round for the main chain can be retrieved (off-chain) using
/// `curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/latest',
/// `curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest',
/// or using the following python script:
/// import time
/// genesis = 1595431050
/// curr_round = (time.time() - genesis) // 30 + 1
/// The round in 5 mins from now will be curr_round + 5*2.
/// genesis = 1692803367
/// curr_round = (time.time() - genesis) // 3 + 1
/// The round in 5 mins from now will be curr_round + 5*20.
/// (genesis is the epoch of the first round as returned from
/// curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/info.)
/// curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/info.)
/// - Anyone can *close* the game to new participants by providing drand's randomness of round N-2 (i.e., 1 minute before
/// round N). The randomness of round X can be retrieved using
/// `curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/X'.
/// `curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/X'.
/// - Users can join the game as long as it is not closed and receive a *ticket*.
/// - Anyone can *complete* the game by proving drand's randomness of round N, which is used to declare the winner.
/// - The owner of the winning "ticket" can request a "winner ticket" and finish the game.
Expand Down Expand Up @@ -96,16 +96,16 @@ module games::drand_based_lottery {
}

/// Anyone can close the game by providing the randomness of round-2.
public entry fun close(game: &mut Game, drand_sig: vector<u8>, drand_prev_sig: vector<u8>) {
public entry fun close(game: &mut Game, drand_sig: vector<u8>) {
assert!(game.status == IN_PROGRESS, EGameNotInProgress);
verify_drand_signature(drand_sig, drand_prev_sig, closing_round(game.round));
verify_drand_signature(drand_sig, closing_round(game.round));
game.status = CLOSED;
}

/// Anyone can complete the game by providing the randomness of round.
public entry fun complete(game: &mut Game, drand_sig: vector<u8>, drand_prev_sig: vector<u8>) {
public entry fun complete(game: &mut Game, drand_sig: vector<u8>) {
assert!(game.status != COMPLETED, EGameAlreadyCompleted);
verify_drand_signature(drand_sig, drand_prev_sig, game.round);
verify_drand_signature(drand_sig, game.round);
game.status = COMPLETED;
// The randomness is derived from drand_sig by passing it through sha2_256 to make it uniform.
let digest = derive_randomness(drand_sig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,10 @@ module games::drand_based_scratch_card {
ticket: Ticket,
game: &Game,
drand_sig: vector<u8>,
drand_prev_sig: vector<u8>,
ctx: &mut TxContext
) {
assert!(ticket.game_id == object::id(game), EInvalidTicket);
drand_lib::verify_drand_signature(drand_sig, drand_prev_sig, end_of_game_round(game.base_drand_round));
drand_lib::verify_drand_signature(drand_sig, end_of_game_round(game.base_drand_round));
// The randomness for the current ticket is derived by HMAC(drand randomness, ticket id).
// A solution like checking if (drand randomness % reward_factor) == (ticket id % reward_factor) is not secure
// as the adversary can control the values of ticket id. (For this particular game this attack is not
Expand Down Expand Up @@ -186,6 +185,6 @@ module games::drand_based_scratch_card {
// at least 24 hours from now. Since the creator does not know as well if its game is created in the beginning
// or the end of the epoch, we define the end of the game to be 24h + 24h from when it started, +1h to be on
// the safe side since epoch duration is not deterministic.
round + 2 * 60 * (24 + 25)
round + 20 * 60 * (24 + 25)
}
}
33 changes: 19 additions & 14 deletions sui_programmability/examples/games/sources/drand_lib.move
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

/// Helper module for working with drand outputs.
/// Currently works with chain 8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce.
/// Currently works with chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 (quicknet).
///
/// See examples in drand_based_lottery.move.
/// See examples of how to use this in drand_based_lottery.move and drand_based_scratch_card.move.
///
/// If you want to use this module with the default network which has a 30s period, you need to change the public key,
/// genesis time and include the previous signature in verify_drand_signature. See https://drand.love/developer/ or the
/// previous version of this file: https://github.com/MystenLabs/sui/blob/92df778310679626f00bc4226d7f7a281322cfdd/sui_programmability/examples/games/sources/drand_lib.move
module games::drand_lib {
use std::hash::sha2_256;
use std::vector;
Expand All @@ -16,21 +19,24 @@ module games::drand_lib {
const EInvalidRndLength: u64 = 0;
const EInvalidProof: u64 = 1;

/// The genesis time of chain 8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce.
const GENESIS: u64 = 1595431050;
/// The public key of chain 8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce.
/// The genesis time of chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971.
const GENESIS: u64 = 1692803367;
/// The public key of chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971.
const DRAND_PK: vector<u8> =
x"868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31";
x"83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a";

/// The time in seconds between randomness beacon rounds.
const PERIOD: u64 = 3;

/// Check that a given epoch time has passed by verifying a drand signature from a later time.
/// round must be at least (epoch_time - GENESIS)/30 + 1).
public fun verify_time_has_passed(epoch_time: u64, sig: vector<u8>, prev_sig: vector<u8>, round: u64) {
assert!(epoch_time <= GENESIS + 30 * (round - 1), EInvalidProof);
verify_drand_signature(sig, prev_sig, round);
/// round must be at least (epoch_time - GENESIS)/PERIOD + 1).
public fun verify_time_has_passed(epoch_time: u64, sig: vector<u8>, round: u64) {
assert!(epoch_time <= GENESIS + PERIOD * (round - 1), EInvalidProof);
verify_drand_signature(sig, round);
}

/// Check a drand output.
public fun verify_drand_signature(sig: vector<u8>, prev_sig: vector<u8>, round: u64) {
public fun verify_drand_signature(sig: vector<u8>, round: u64) {
// Convert round to a byte array in big-endian order.
let round_bytes: vector<u8> = vector[0, 0, 0, 0, 0, 0, 0, 0];
let i = 7;
Expand All @@ -43,10 +49,9 @@ module games::drand_lib {
};

// Compute sha256(prev_sig, round_bytes).
vector::append(&mut prev_sig, round_bytes);
let digest = sha2_256(prev_sig);
let digest = sha2_256(round_bytes);
// Verify the signature on the hash.
assert!(bls12381::bls12381_min_pk_verify(&sig, &DRAND_PK, &digest), EInvalidProof);
assert!(bls12381::bls12381_min_sig_verify(&sig, &DRAND_PK, &digest), EInvalidProof);
}

/// Derive a uniform vector from a drand signature.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ module games::drand_based_lottery_tests {
#[test]
fun test_verify_time_has_passed_success() {
// Taken from the output of
// curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/8
// curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/8
verify_time_has_passed(
1595431050 + 30*7, // exactly the 8th round
x"b3ed3c540ef5c5407ea6dbf7407ca5899feeb54f66f7e700ee063db71f979a869d28efa9e10b5e6d3d24a838e8b6386a15b411946c12815d81f2c445ae4ee1a7732509f0842f327c4d20d82a1209f12dbdd56fd715cc4ed887b53c321b318cd7",
x"ada04f01558359fec41abeee43c5762c4017476a1e64ad643d3378a50ac1f7d07ad0abf0ba4bada53e6762582d661a980adf6290b5fb1683dedd821fe192868d70624907b2cef002e3ee197acd2395f1406fb660c91337d505860ab306a4432e",
1692803367 + 3*7, // exactly the 8th round
x"a0c06b9964123d2e6036aa004c140fc301f4edd3ea6b8396a15dfd7dfd70cc0dce0b4a97245995767ab72cf59de58c47",
8
);
verify_time_has_passed(
1595431050 + 30*7 - 10, // the 8th round - 10 seconds
x"b3ed3c540ef5c5407ea6dbf7407ca5899feeb54f66f7e700ee063db71f979a869d28efa9e10b5e6d3d24a838e8b6386a15b411946c12815d81f2c445ae4ee1a7732509f0842f327c4d20d82a1209f12dbdd56fd715cc4ed887b53c321b318cd7",
x"ada04f01558359fec41abeee43c5762c4017476a1e64ad643d3378a50ac1f7d07ad0abf0ba4bada53e6762582d661a980adf6290b5fb1683dedd821fe192868d70624907b2cef002e3ee197acd2395f1406fb660c91337d505860ab306a4432e",
1692803367 + 3*7 - 2, // the 8th round - 2 seconds
x"a0c06b9964123d2e6036aa004c140fc301f4edd3ea6b8396a15dfd7dfd70cc0dce0b4a97245995767ab72cf59de58c47",
8
);
}
Expand All @@ -29,11 +27,10 @@ module games::drand_based_lottery_tests {
#[expected_failure(abort_code = games::drand_lib::EInvalidProof)]
fun test_verify_time_has_passed_failure() {
// Taken from the output of
// curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/8
// curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/8
verify_time_has_passed(
1595431050 + 30*8, // exactly the 9th round - 10 seconds
x"b3ed3c540ef5c5407ea6dbf7407ca5899feeb54f66f7e700ee063db71f979a869d28efa9e10b5e6d3d24a838e8b6386a15b411946c12815d81f2c445ae4ee1a7732509f0842f327c4d20d82a1209f12dbdd56fd715cc4ed887b53c321b318cd7",
x"ada04f01558359fec41abeee43c5762c4017476a1e64ad643d3378a50ac1f7d07ad0abf0ba4bada53e6762582d661a980adf6290b5fb1683dedd821fe192868d70624907b2cef002e3ee197acd2395f1406fb660c91337d505860ab306a4432e",
1692803367 + 3*8, // exactly the 9th round - 10 seconds
x"a0c06b9964123d2e6036aa004c140fc301f4edd3ea6b8396a15dfd7dfd70cc0dce0b4a97245995767ab72cf59de58c47",
8
);
}
Expand Down Expand Up @@ -69,36 +66,34 @@ module games::drand_based_lottery_tests {
// User 2 closes the game.
test_scenario::next_tx(scenario, user2);
// Taken from the output of
// curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/8
// curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/8
drand_based_lottery::close(
game,
x"b3ed3c540ef5c5407ea6dbf7407ca5899feeb54f66f7e700ee063db71f979a869d28efa9e10b5e6d3d24a838e8b6386a15b411946c12815d81f2c445ae4ee1a7732509f0842f327c4d20d82a1209f12dbdd56fd715cc4ed887b53c321b318cd7",
x"ada04f01558359fec41abeee43c5762c4017476a1e64ad643d3378a50ac1f7d07ad0abf0ba4bada53e6762582d661a980adf6290b5fb1683dedd821fe192868d70624907b2cef002e3ee197acd2395f1406fb660c91337d505860ab306a4432e"
x"a0c06b9964123d2e6036aa004c140fc301f4edd3ea6b8396a15dfd7dfd70cc0dce0b4a97245995767ab72cf59de58c47",
);

// User3 completes the game.
test_scenario::next_tx(scenario, user3);
// Taken from theoutput of
// curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/8
// curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/10
drand_based_lottery::complete(
game,
x"aec34e398bb53efc192ef6b91ad6960689aefa2c8326c521523d922849bb8bc16e76872640e7a1dd656e94772d9fd4ae19a63a10854a0853505bd3c8c5b8fff109ff260b0566b5ac93d2b0d8fecc9b08f7ad5101a253913f55a0c53f45c15c7f",
x"99c37c83a0d7bb637f0e2f0c529aa5c8a37d0287535debe5dacd24e95b6e38f3394f7cb094bdf4908a192a3563276f951948f013414d927e0ba8c84466b4c9aea4de2a253dfec6eb5b323365dfd2d1cb98184f64c22c5293c8bfe7962d4eb0f5"
x"ac415e508c484053efed1c6c330e3ae0bf20185b66ed088864dac1ff7d6f927610824986390d3239dac4dd73e6f865f5",
);

// User3 is the winner since the mod of the hash results in 2.
test_scenario::next_tx(scenario, user3);
assert!(!test_scenario::has_most_recent_for_address<GameWinner>(user3), 1);
let ticket = test_scenario::take_from_address<Ticket>(scenario, user3);
// User2 is the winner since the mod of the hash results in 1.
test_scenario::next_tx(scenario, user2);
assert!(!test_scenario::has_most_recent_for_address<GameWinner>(user2), 1);
let ticket = test_scenario::take_from_address<Ticket>(scenario, user2);
let ticket_game_id = *drand_based_lottery::get_ticket_game_id(&ticket);
drand_based_lottery::redeem(&ticket, &game_val, test_scenario::ctx(scenario));
drand_based_lottery::delete_ticket(ticket);

// Make sure User3 now has a winner ticket for the right game id.
test_scenario::next_tx(scenario, user3);
let ticket = test_scenario::take_from_address<GameWinner>(scenario, user3);
// Make sure User2 now has a winner ticket for the right game id.
test_scenario::next_tx(scenario, user2);
let ticket = test_scenario::take_from_address<GameWinner>(scenario, user2);
assert!(drand_based_lottery::get_game_winner_game_id(&ticket) == &ticket_game_id, 1);
test_scenario::return_to_address(user3, ticket);
test_scenario::return_to_address(user2, ticket);

test_scenario::return_shared(game_val);
test_scenario::end(scenario_val);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module games::drand_based_scratch_card_tests {
let game = test_scenario::take_immutable<drand_based_scratch_card::Game>(scenario);
let reward_val = test_scenario::take_shared<drand_based_scratch_card::Reward>(scenario);
let drand_final_round = drand_based_scratch_card::end_of_game_round(drand_based_scratch_card::get_game_base_drand_round(&game));
assert!(drand_final_round == 5890, 1);
assert!(drand_final_round == 58810, 1);

// Since everything here is deterministic, we know that the 49th ticket will be a winner.
let i = 0;
Expand All @@ -48,12 +48,11 @@ module games::drand_based_scratch_card_tests {
test_scenario::next_tx(scenario, user2);
let ticket = test_scenario::take_from_sender<drand_based_scratch_card::Ticket>(scenario);
// Generated using:
// curl https://drand.cloudflare.com/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/public/5890
// curl https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/58810
drand_based_scratch_card::evaluate(
ticket,
&game,
x"98a99ec46b8bda71fbb0a90a0c6a02e0be92803d756ff66b386fd7647a6071fc33116cd94b0a0f09b48d78e399b6ca590868213a2bb85be829841fdac9487f89b5ce02c2d13d38240d40ce9868bd17f903cf5fba5b825769cdbf38c22cebc6a5",
x"b0ee0f3d50e7ed6d5860ce9addbada59fbde444745fedd46f8a15dd48f5c3524adeed06d54775b24bf652aadd087cf790a2f7c55bd81ea29f594d3c68a93bb5c3a595af0f1a368c762c07113f683abad50d7fceb1048e7376deb6febcb4683c6",
x"876b8586ed9522abd0ca596d6e214e9a7e9bedc4a2e9698d27970e892287268062aba93fd1a7c24fcc188a4c7f0a0e98",
test_scenario::ctx(scenario)
);
test_scenario::next_tx(scenario, user2);
Expand All @@ -63,7 +62,7 @@ module games::drand_based_scratch_card_tests {
i = i + 1;
};
// This value may change if the object ID is changed.
assert!(i == 21, 1);
assert!(i == 3, 1);

// Claim the reward.
let winner = test_scenario::take_from_sender<drand_based_scratch_card::Winner>(scenario);
Expand Down

0 comments on commit ef5b712

Please sign in to comment.