From d43a7addde2f2f538bd2558361fa0e1aad5de7e8 Mon Sep 17 00:00:00 2001 From: Ashok Menon Date: Fri, 12 Jul 2024 22:16:39 +0100 Subject: [PATCH] [Examples/Move] Port remaining Crypto Sui Programmability Examples (#18609) ## Description Port over the following examples from sui_programmability/examples: - crypto/sources/ecdsa.move - crypto/sources/groth16.move - ~games/sources/drand_lib.move~ - ~games/sources/drand_based_lottery.move~ - ~games/sources/drand_based_scratch_card.move~ - games/sources/vdf_based_lottery.move Modernising and cleaning them up in the process: - Applying wrapping consistently at 100 characters, and cleaning up comments. - Removing unnecessary use of `entry` functions, including returning values instead of transfering to sender in some cases. - Using receiver functions where possible. - Standardising file order and adding titles for sections. - Standardising use of doc comments vs regular comments. - Using clever errors. This marks the final set of examples to be moved out of sui-programmability, which will then be deleted. ## Test plan ``` sui-framework-tests$ cargo nextest run -- run_examples_move_unit_tests ``` ## Stack - #18525 - #18526 - #18557 - #18558 - #18595 --- ## Release notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: --------- Co-authored-by: Ronny Roland --- examples/move/crypto/ecdsa_k1/Move.toml | 10 + .../move/crypto/ecdsa_k1/sources/example.move | 119 +++++++++ examples/move/crypto/groth16/Move.toml | 10 + .../move/crypto/groth16/sources/example.move | 108 ++++++++ .../crypto/groth16/tests/example_tests.move | 34 +++ examples/move/vdf/Move.toml | 10 + examples/move/vdf/sources/lottery.move | 231 ++++++++++++++++++ examples/move/vdf/tests/lottery_tests.move | 155 ++++++++++++ 8 files changed, 677 insertions(+) create mode 100644 examples/move/crypto/ecdsa_k1/Move.toml create mode 100644 examples/move/crypto/ecdsa_k1/sources/example.move create mode 100644 examples/move/crypto/groth16/Move.toml create mode 100644 examples/move/crypto/groth16/sources/example.move create mode 100644 examples/move/crypto/groth16/tests/example_tests.move create mode 100644 examples/move/vdf/Move.toml create mode 100644 examples/move/vdf/sources/lottery.move create mode 100644 examples/move/vdf/tests/lottery_tests.move diff --git a/examples/move/crypto/ecdsa_k1/Move.toml b/examples/move/crypto/ecdsa_k1/Move.toml new file mode 100644 index 0000000000000..8ebda25a8188a --- /dev/null +++ b/examples/move/crypto/ecdsa_k1/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "ecdsa_k1" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +ecdsa_k1 = "0x0" diff --git a/examples/move/crypto/ecdsa_k1/sources/example.move b/examples/move/crypto/ecdsa_k1/sources/example.move new file mode 100644 index 0000000000000..293012d61a9e0 --- /dev/null +++ b/examples/move/crypto/ecdsa_k1/sources/example.move @@ -0,0 +1,119 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A basic ECDSA utility contract to do the following: +/// +/// 1) Hash a piece of data using Keccak256, output an object with hashed data. +/// 2) Recover a Secp256k1 signature to its public key, output an +/// object with the public key. +/// 3) Verify a Secp256k1 signature, produce an event for whether it is verified. +module ecdsa_k1::example { + use sui::ecdsa_k1; + use sui::event; + + // === Object Types === + + /// Object that holds the output data + public struct Output has key, store { + id: UID, + value: vector + } + + // == Event Types === + + /// Event on whether the signature is verified + public struct VerifiedEvent has copy, drop { + is_verified: bool, + } + + // === Public Functions === + + /// Hash the data using Keccak256, output an object with the hash to recipient. + public fun keccak256( + data: vector, + recipient: address, + ctx: &mut TxContext, + ) { + let hashed = Output { + id: object::new(ctx), + value: sui::hash::keccak256(&data), + }; + // Transfer an output data object holding the hashed data to the recipient. + transfer::public_transfer(hashed, recipient) + } + + /// Recover the public key using the signature and message, assuming the signature was produced + /// over the Keccak256 hash of the message. Output an object with the recovered pubkey to + /// recipient. + public fun ecrecover( + signature: vector, + msg: vector, + recipient: address, + ctx: &mut TxContext, + ) { + let pubkey = Output { + id: object::new(ctx), + value: ecdsa_k1::secp256k1_ecrecover(&signature, &msg, 0), + }; + // Transfer an output data object holding the pubkey to the recipient. + transfer::public_transfer(pubkey, recipient) + } + + /// Recover the Ethereum address using the signature and message, assuming the signature was + /// produced over the Keccak256 hash of the message. Output an object with the recovered address + /// to recipient. + public fun ecrecover_to_eth_address( + mut signature: vector, + msg: vector, + recipient: address, + ctx: &mut TxContext, + ) { + // Normalize the last byte of the signature to be 0 or 1. + let v = &mut signature[64]; + if (*v == 27) { + *v = 0; + } else if (*v == 28) { + *v = 1; + } else if (*v > 35) { + *v = (*v - 1) % 2; + }; + + // Ethereum signature is produced with Keccak256 hash of the message, so the last param is + // 0. + let pubkey = ecdsa_k1::secp256k1_ecrecover(&signature, &msg, 0); + let uncompressed = ecdsa_k1::decompress_pubkey(&pubkey); + + // Take the last 64 bytes of the uncompressed pubkey. + let mut uncompressed_64 = vector[]; + let mut i = 1; + while (i < 65) { + uncompressed_64.push_back(uncompressed[i]); + i = i + 1; + }; + + // Take the last 20 bytes of the hash of the 64-bytes uncompressed pubkey. + let hashed = sui::hash::keccak256(&uncompressed_64); + let mut addr = vector[]; + let mut i = 12; + while (i < 32) { + addr.push_back(hashed[i]); + i = i + 1; + }; + + let addr_object = Output { + id: object::new(ctx), + value: addr, + }; + + // Transfer an output data object holding the address to the recipient. + transfer::public_transfer(addr_object, recipient) + } + + /// Verified the secp256k1 signature using public key and message assuming Keccak was using when + /// signing. Emit an is_verified event of the verification result. + public fun secp256k1_verify(signature: vector, public_key: vector, msg: vector) { + event::emit(VerifiedEvent { + is_verified: ecdsa_k1::secp256k1_verify(&signature, &public_key, &msg, 0) + }); + } +} diff --git a/examples/move/crypto/groth16/Move.toml b/examples/move/crypto/groth16/Move.toml new file mode 100644 index 0000000000000..28c7c3286680b --- /dev/null +++ b/examples/move/crypto/groth16/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "groth16" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +groth16 = "0x0" diff --git a/examples/move/crypto/groth16/sources/example.move b/examples/move/crypto/groth16/sources/example.move new file mode 100644 index 0000000000000..86bb49d844e11 --- /dev/null +++ b/examples/move/crypto/groth16/sources/example.move @@ -0,0 +1,108 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A verifier for the Groth16 zk-SNARK over the BLS12-381 construction. +/// See https://eprint.iacr.org/2016/260.pdf for details. +module groth16::example { + use sui::bls12381; + use sui::group_ops::Element; + + // === Types === + + /// A Groth16 proof. + public struct Proof has drop { + a: Element, + b: Element, + c: Element, + } + + /// A Groth16 verifying key used to verify a zero-knowledge proof. + public struct VerifyingKey has store, drop { + alpha: Element, + beta: Element, + gamma: Element, + gamma_abc: vector>, + delta: Element, + } + + /// A prepared verifying key. This makes verification faster than using the verifying key directly. + public struct PreparedVerifyingKey has store, drop { + alpha_beta: Element, + gamma_neg: Element, + gamma_abc: vector>, + delta_neg: Element, + } + + // === Errors === + + #[error] + const EInvalidNumberOfPublicInputs: vector = + b"There must be one more public input than gamma_abc entries in the verifying key."; + + // === Public Functions === + + /// Create a new `Proof`. + public fun create_proof( + a: Element, + b: Element, + c: Element, + ): Proof { + Proof { a, b, c } + } + + /// Create a new `VerifyingKey`. + public fun create_verifying_key( + alpha: Element, + beta: Element, + gamma: Element, + gamma_abc: vector>, + delta: Element, + ): VerifyingKey { + VerifyingKey { alpha, beta, gamma, gamma_abc, delta } + } + + /// Create a PreparedVerifyingKey from a VerifyingKey. This only have to be + /// done once. + public fun prepare(vk: VerifyingKey): PreparedVerifyingKey { + PreparedVerifyingKey { + alpha_beta: bls12381::pairing(&vk.alpha, &vk.beta), + gamma_neg: bls12381::g2_neg(&vk.gamma), + gamma_abc: vk.gamma_abc, + delta_neg: bls12381::g2_neg(&vk.delta), + } + } + + /// Verify a Groth16 proof with some public inputs and a verifying key. + public fun verify( + pvk: &PreparedVerifyingKey, + proof: &Proof, + public_inputs: &vector>, + ): bool { + let prepared_inputs = prepare_inputs(&pvk.gamma_abc, public_inputs); + let mut lhs = bls12381::pairing(&proof.a, &proof.b); + lhs = bls12381::gt_add(&lhs, &bls12381::pairing(&prepared_inputs, &pvk.gamma_neg)); + lhs = bls12381::gt_add(&lhs, &bls12381::pairing(&proof.c, &pvk.delta_neg)); + lhs == pvk.alpha_beta + } + + // === Helpers === + + fun prepare_inputs( + vk_gamma_abc: &vector>, + public_inputs: &vector>, + ): Element { + let length = public_inputs.length(); + assert!(length + 1 == vk_gamma_abc.length(), EInvalidNumberOfPublicInputs); + + let mut output = vk_gamma_abc[0]; + let mut i = 0; + while (i < length) { + output = bls12381::g1_add( + &output, + &bls12381::g1_mul(&public_inputs[i], &vk_gamma_abc[i + 1]), + ); + i = i + 1; + }; + output + } +} diff --git a/examples/move/crypto/groth16/tests/example_tests.move b/examples/move/crypto/groth16/tests/example_tests.move new file mode 100644 index 0000000000000..95e31adc898a2 --- /dev/null +++ b/examples/move/crypto/groth16/tests/example_tests.move @@ -0,0 +1,34 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module groth16::example_tests { + use sui::bls12381; + use groth16::example::{create_verifying_key, create_proof, verify}; + + #[test] + fun test_verification() { + let vk = create_verifying_key( + bls12381::g1_from_bytes(&x"b58cfc3b0f43d98e7dbe865af692577d52813cb62ef3c355215ec3be2a0355a1ae5da76dd3e626f8a60de1f4a8138dee"), + bls12381::g2_from_bytes(&x"9047b42915b32ef9dffe3acc0121a1450416e7f9791159f165ab0729d744da3ed82cd4822ca1d7fef35147cfd620b59b0ca09db7dff43aab6c71635ba8f86a83f058e9922e5cdacbe21d0e5e855cf1e776a61b272c12272fe526f5ba3b48d579"), + bls12381::g2_from_bytes(&x"ad7c5a6cefcae53a3fbae72662c7c04a2f8e1892cb83615a02b32c31247172b7f317489b84e72f14acaf4f3e9ed18141157c6c1464bf15d957227f75a3c550d6d27f295b41a753340c6eec47b471b2cb8664c84f3e9b725325d3fb8afc6b56d0"), + vector[ + bls12381::g1_from_bytes(&x"b2c9c61ccc28e913284a47c34e60d487869ff423dd574db080d35844f9eddd2b2967141b588a35fa82a278ce39ae6b1a"), + bls12381::g1_from_bytes(&x"9026ae12d58d203b4fc5dfad4968cbf51e43632ed1a05afdcb2e380ee552b036fbefc7780afe9675bcb60201a2421b2c") + ], + bls12381::g2_from_bytes(&x"b1294927d02f8e86ac57c3b832f4ecf5e03230445a9a785ac8d25cf968f48cca8881d0c439c7e8870b66567cf611da0c1734316632f39d3125c8cecca76a8661db91cbfae217547ea1fc078a24a1a31555a46765011411094ec649d42914e2f5"), + ); + + let public_inputs = vector[ + bls12381::scalar_from_bytes(&x"46722abc81a82d01ac89c138aa01a8223cb239ceb1f02cdaad7e1815eb997ca6") + ]; + + let proof = create_proof( + bls12381::g1_from_bytes(&x"9913bdcabdff2cf1e7dea1673c5edba5ed6435df2f2a58d6d9e624609922bfa3976a98d991db333812bf6290a590afaa"), + bls12381::g2_from_bytes(&x"b0265b35af5069593ee88626cf3ba9a0f07699510a25aec3a27048792ab83b3467d6b814d1c09c412c4dcd7656582e6607b72915081c82794ccedf643c27abace5b23a442079d8dcbd0d68dd697b8e0b699a1925a5f2c77f5237efbbbeda3bd0"), + bls12381::g1_from_bytes(&x"b1237cf48ca7aa98507e826aac336b9e24f14133de1923fffac602a1203b795b3037c4c94e7246bacee7b2757ae912e5"), + ); + + assert!(vk.prepare().verify(&proof, &public_inputs)); + } +} diff --git a/examples/move/vdf/Move.toml b/examples/move/vdf/Move.toml new file mode 100644 index 0000000000000..3f835c6add095 --- /dev/null +++ b/examples/move/vdf/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "VDF" +version = "0.0.1" +edition = "2024.beta" + +[dependencies] +Sui = { local = "../../../crates/sui-framework/packages/sui-framework" } + +[addresses] +vdf = "0x0" diff --git a/examples/move/vdf/sources/lottery.move b/examples/move/vdf/sources/lottery.move new file mode 100644 index 0000000000000..f61525f6b8159 --- /dev/null +++ b/examples/move/vdf/sources/lottery.move @@ -0,0 +1,231 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A basic lottery game that depends on user-provided randomness which is processed by a verifiable +/// delay function (VDF) to make sure that it is unbiasable. +/// +/// During the submission phase, players can buy tickets. When buying a ticket, a user must provide +/// some randomness `r`. This randomness is added to the combined randomness of the lottery, `h`, as +/// `h = Sha2_256(h, r)`. +/// +/// After the submission phase has ended, the combined randomness is used to generate an input to +/// the VDF. Anyone may now compute the output and submit it along with a proof of correctness to +/// the `complete` function. If the output and proof verifies, the game ends, and the hash of the +/// output is used to pick a winner. +/// +/// The outcome is guaranteed to be fair if: +/// +/// 1) At least one player contributes true randomness, +/// +/// 2) The number of iterations is defined such that it takes at least `submission_phase_length` to +/// compute the VDF. +module vdf::lottery { + use sui::clock::Clock; + use std::hash::sha2_256; + use sui::vdf::{hash_to_input, vdf_verify}; + + // === Receiver Functions === + + public use fun delete_ticket as Ticket.delete; + public use fun delete_game_winner as GameWinner.delete; + public use fun ticket_game_id as Ticket.game_id; + public use fun game_winner_game_id as GameWinner.game_id; + + // === Object Types === + + /// Game represents a set of parameters of a single game. + /// This game can be extended to require ticket purchase, reward winners, etc. + public struct Game has key { + id: UID, + iterations: u64, + status: u8, + timestamp_start: u64, + submission_phase_length: u64, + participants: u64, + vdf_input_seed: vector, + winner: Option, + } + + /// Ticket represents a participant in a single game. + /// Can be deconstructed only by the owner. + public struct Ticket has key, store { + id: UID, + game_id: ID, + participant_index: u64, + } + + /// GameWinner represents a participant that won in a specific game. + /// Can be deconstructed only by the owner. + public struct GameWinner has key, store { + id: UID, + game_id: ID, + } + + // === Error Codes === + + #[error] + const EGameNotInProgress: vector = + b"Lottery not in progress, cannot participate."; + + #[error] + const EGameAlreadyCompleted: vector = + b"Lottery winner has already been selected"; + + #[error] + const EInvalidTicket: vector = + b"Ticket does not match lottery"; + + #[error] + const ENotWinner: vector = + b"Not the winning ticket"; + + #[error] + const ESubmissionPhaseInProgress: vector = + b"Cannot call winner or redeem funds until submission phase has completed."; + + #[error] + const EInvalidVdfProof: vector = + b"Invalid VDF Proof"; + + #[error] + const ESubmissionPhaseFinished: vector = + b"Cannot participate in a finished lottery."; + + #[error] + const EInvalidRandomness: vector = + b"Randomness length is not correct"; + + // === Constants === + + // Game status + const IN_PROGRESS: u8 = 0; + const COMPLETED: u8 = 1; + + const RANDOMNESS_LENGTH: u64 = 16; + + // === Public Functions === + + /// Create a shared-object Game. + public fun create(iterations: u64, submission_phase_length: u64, clock: &Clock, ctx: &mut TxContext) { + transfer::share_object(Game { + id: object::new(ctx), + iterations, + status: IN_PROGRESS, + timestamp_start: clock.timestamp_ms(), + submission_phase_length, + vdf_input_seed: vector::empty(), + participants: 0, + winner: option::none(), + }); + } + + /// Anyone can participate in the game and receive a ticket. + public fun participate( + self: &mut Game, + my_randomness: vector, + clock: &Clock, + ctx: &mut TxContext, + ): Ticket { + assert!(self.status == IN_PROGRESS, EGameNotInProgress); + assert!( + clock.timestamp_ms() - self.timestamp_start < self.submission_phase_length, + ESubmissionPhaseFinished, + ); + + // Update combined randomness by concatenating the provided randomness and hashing it + assert!(my_randomness.length() == RANDOMNESS_LENGTH, EInvalidRandomness); + self.vdf_input_seed.append(my_randomness); + self.vdf_input_seed = sha2_256(self.vdf_input_seed); + + // Assign index to this participant + let participant_index = self.participants; + self.participants = self.participants + 1; + + Ticket { + id: object::new(ctx), + game_id: object::id(self), + participant_index, + } + } + + /// Complete this lottery by sending VDF output and proof for the seed created from the + /// contributed randomness. Anyone can call this. + public fun complete( + self: &mut Game, + vdf_output: vector, + vdf_proof: vector, + clock: &Clock, + ) { + assert!(self.status != COMPLETED, EGameAlreadyCompleted); + assert!( + clock.timestamp_ms() - self.timestamp_start >= self.submission_phase_length, + ESubmissionPhaseInProgress, + ); + + // Hash combined randomness to vdf input + let vdf_input = hash_to_input(&self.vdf_input_seed); + + // Verify output and proof + assert!(vdf_verify(&vdf_input, &vdf_output, &vdf_proof, self.iterations), EInvalidVdfProof); + + // The randomness is derived from the VDF output by passing it through a hash function with + // uniformly distributed output to make it uniform. Any hash function with uniformly + // distributed output can be used. + let randomness = sha2_256(vdf_output); + + // Set winner and mark lottery completed + self.winner = option::some(safe_selection(self.participants, &randomness)); + self.status = COMPLETED; + } + + /// The winner can redeem its ticket. + public fun redeem(self: &Game, ticket: &Ticket, ctx: &mut TxContext): GameWinner { + assert!(self.status == COMPLETED, ESubmissionPhaseInProgress); + assert!(object::id(self) == ticket.game_id, EInvalidTicket); + assert!(self.winner.contains(&ticket.participant_index), ENotWinner); + + GameWinner { + id: object::new(ctx), + game_id: ticket.game_id, + } + } + + // Note that a ticket can be deleted before the game was completed. + public fun delete_ticket(ticket: Ticket) { + let Ticket { id, game_id: _, participant_index: _} = ticket; + object::delete(id); + } + + public fun delete_game_winner(ticket: GameWinner) { + let GameWinner { id, game_id: _} = ticket; + object::delete(id); + } + + public fun ticket_game_id(ticket: &Ticket): &ID { + &ticket.game_id + } + + public fun game_winner_game_id(ticket: &GameWinner): &ID { + &ticket.game_id + } + + // === Private Helpers === + + // Converts the first 16 bytes of rnd to a u128 number and outputs its modulo with input n. + // Since n is u64, the output is at most 2^{-64} biased assuming rnd is uniformly random. + fun safe_selection(n: u64, rnd: &vector): u64 { + assert!(rnd.length() >= 16, EInvalidRandomness); + let mut m: u128 = 0; + let mut i = 0; + while (i < 16) { + m = m << 8; + let curr_byte = rnd[i]; + m = m + (curr_byte as u128); + i = i + 1; + }; + let n_128 = (n as u128); + let module_128 = m % n_128; + let res = (module_128 as u64); + res + } +} diff --git a/examples/move/vdf/tests/lottery_tests.move b/examples/move/vdf/tests/lottery_tests.move new file mode 100644 index 0000000000000..850af7e49091e --- /dev/null +++ b/examples/move/vdf/tests/lottery_tests.move @@ -0,0 +1,155 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module vdf::lottery_tests { + use sui::test_scenario as ts; + use sui::clock; + use vdf::lottery::{Self, Game, GameWinner}; + + const OUTPUT: vector = + x"c0014d00b5e624fe10d1cc1e593c0ffb8c3084e49bb70efc4337640a73990bb29dfb430b55710475bcc7524c77627d8067415fffa63e0e84b1204225520fea384999719c66dbdc6e91863d99c64674af971631b56e22b7cc780765bf12d53edea1dadf566f80a62769e287a1e195596d4894b2e1360e451cbf06864762275b4d5063871d45627dea2e42ab93d5345bf172b9724216627abbf295b35a8e64e13e585bca54848a90212c9f7a3adffc25c3b87eefa7d4ab1660b523bf6410b9a9ea0e00c001327d73bebc768d150beb2a1b0de9e80c69ed594ae7787d548af44eb1e0a03616100133146c9c1202ea3a35c331864f3bfe59ffa3c88d6acb8af7a4b1b5ea842c4c4c88d415539389e614876d738d80217a3ad16d001f2de60f62b04a9d8de7ccb4716c3368f0d42e3e719dbb07bdb4355f0e5569714fbcc130f30ac7b49a5b207a444c7e00a0c27edae10c28b05f87545f337283f90c4e4ed69639683154d6a89e6589db4d18702d3a29705b434dc32e10fcbd3c62d1da20b45dba511bcecdf7c101009db7911c39f3937785dd8bd0e9db56c94777bdd867897493f82e0931e0e5facb730c3aa400f815f3d2f61de94373209dcbf9210c5e1f179b675adce2be7159238cb5f89c568004ccfc75d1b3de99a060255bcd6c9dd9a674844251ec27c48119b870f4ce63cac2081541c4269bbfa60148f9dbf2c60c76099a6538a33e88c24ce092e6aad9bdfff330470ffb87b01b666dd98e44843b41896f2be688361fe062692b441b4dd8f8ecfa96948d13daf486259bb8934cab7e9d9788da0eac7edf56"; + + const PROOF: vector = + x"c0010f1ea16b16f3fc46eb0b00a516092a6ab389e259a3013eee5e39e130702de85954b8aac435e724ad0bfd210ab7789fb91b54ac4352154f3251e0a87ccfd2c9a57d26468a384f527e129fc82b33c04b3ebbec3a99967798a95b39820c35ea015fdf4c81e143004b34b99e63462cf350689b2abdd6c3903adcbe55781d3a89c89dc571c312f9a80911a9d64884747319574b3a4ded25478e6d64b9cfb25d9c67366bc25d9ac99bcdba16665158da50a2ba179893292c4b7e76502ecaba1337d693c001fb3867669e0d4e45aa43d959dbe33c3d35b00e8414d1cf1bb9552726bb95bafa0a2c12a014a3b8fb0bd5ab9a40430ff59364b19d58d80665fee0bfee272a38c45413a3688832bf9bcacf7b5723436c120878f85ce084e72b13246ecfec7cd6a5d79e13296bbb51af785c10afe6c4f07f43a5bc711dc398271185d700b1695310d8e428ad3bc6b81a4faac2f5009b723460dbd260c940dfac06e34854d204dc779f94ab3f67847a7b90855dadc3962871c022e172e96b39a08648e045e14dad87c10102f976797f14be801a441f19771a4835640a74cf7c6ad216f18d9cdaf461bb56a897b804e053cd6cc68d659bd9f0ed985f094932d306c1bd76450bd349db3a81008d7591bc826a36583c3c361add7a8f245d18007d79704d79ae27eb08b52a44af17e2f23b441919049f061d69bac3a09c3e15074e4d75cf82f42dbff1c62ddc94fe6167ccb7265e7eab0def7d30d97be441ad763705dd30d4040815996e34643bf6d7a4f06c22aa5d6d5dd30253ea8aa59607724bb71f5425a5e7fee03b7e9fe8"; + + const BAD_PROOF: vector = + x"0101010180032cf35709e1301d02b40a0dbe3dadfe6ec1eeba8fb8060a1decd0c7a126ea3f27fadcad81435601b0e0abca5c89173ef639e5a88043aa29801e6799e430b509e479b57af981f9ddd48d3a8d5919f99258081557a08270bb441233c78030a01e03ec199b5e3eef5ccc9b1a3d4841cbe4ff529c22a8cd1b1b0075338d864e3890942df6b007d2c3e3a8ef1ce7490c6bbec5372adfcbf8704a1ffc9a69db8d9cdc54762f019036e450e457325eef74b794f3f16ff327d68079a5b9de49163d7323937374f8a785a8f9afe84d6a71b336e4de00f239ee3af1d7604a3985e610e1603bd0e1a4998e19fa0c8920ffd8d61b0a87eeee50ac7c03ff7c4708a34f3bc92fd0103758c954ee34032cee2c78ad8cdc79a35dbc810196b7bf6833e1c45c83b09c0d1b78bc6f8753e10770e7045b08d50b4aa16a75b27a096d5ec1331f1fd0a44e95a8737c20240c90307b5497d3470393c2a00da0649e86d13e820591296c644fc1eef9e7c6ca4967c5e19df3153cd7fbd598c271e11c10397349ddc8cc8452ec"; + + #[test] + #[expected_failure(abort_code = vdf::lottery::ESubmissionPhaseInProgress)] + fun test_complete_too_early() { + let user1 = @0x0; + + let mut ts = ts::begin(user1); + let mut clock = clock::create_for_testing(ts.ctx()); + + lottery::create(1000, 1000, &clock, ts.ctx()); + ts.next_tx(user1); + let mut game: Game = ts.take_shared(); + + // User 1 buys a ticket. + ts.next_tx(user1); + let _t1 = game.participate(b"user1 randomness", &clock, ts.ctx()); + + // Increment time but still in submission phase + clock.increment_for_testing(500); + + // User1 tries to complete the lottery too early. + ts.next_tx(user1); + game.complete(OUTPUT, PROOF, &clock); + abort 0 + } + + #[test] + fun test_play_vdf_lottery() { + let user1 = @0x0; + let user2 = @0x1; + let user3 = @0x2; + let user4 = @0x3; + + let mut ts = ts::begin(user1); + let mut clock = clock::create_for_testing(ts.ctx()); + + lottery::create(1000, 1000, &clock, ts.ctx()); + ts.next_tx(user1); + let mut game: Game = ts.take_shared(); + + // User 1 buys a ticket. + ts.next_tx(user1); + let t1 = game.participate(b"user1 randomness", &clock, ts.ctx()); + + // User 2 buys a ticket. + ts.next_tx(user2); + let t2 = game.participate(b"user2 randomness", &clock, ts.ctx()); + + // User 3 buys a ticket + ts.next_tx(user3); + let t3 = game.participate(b"user3 randomness", &clock, ts.ctx()); + + // User 4 buys a ticket + ts.next_tx(user4); + let t4 = game.participate(b"user4 randomness", &clock, ts.ctx()); + + // Increment time to after submission phase has ended + clock.increment_for_testing(1000); + + // User 3 completes by submitting output and proof of the VDF + ts.next_tx(user3); + game.complete(OUTPUT, PROOF, &clock); + + // User 1 is the winner since the mod of the hash results in 0. + ts.next_tx(user1); + assert!(!ts::has_most_recent_for_address(user1), 1); + let winner = game.redeem(&t1, ts.ctx()); + + // Make sure User1 now has a winner ticket for the right game id. + ts.next_tx(user1); + assert!(winner.game_id() == t1.game_id(), 1); + + t1.delete(); + t2.delete(); + t3.delete(); + t4.delete(); + winner.delete(); + + clock.destroy_for_testing(); + ts::return_shared(game); + ts.end(); + } + + #[test] + #[expected_failure(abort_code = vdf::lottery::EInvalidVdfProof)] + fun test_invalid_vdf_output() { + let user1 = @0x0; + let user2 = @0x1; + let user3 = @0x2; + let user4 = @0x3; + + let mut ts = ts::begin(user1); + let mut clock = clock::create_for_testing(ts.ctx()); + + lottery::create(1000, 1000, &clock, ts.ctx()); + ts.next_tx(user1); + let mut game: Game = ts.take_shared(); + + // User1 buys a ticket. + ts.next_tx(user1); + let _t1 = game.participate(b"user1 randomness", &clock, ts.ctx()); + // User2 buys a ticket. + ts.next_tx(user2); + let _t2 = game.participate(b"user2 randomness", &clock, ts.ctx()); + // User3 buys a ticket + ts.next_tx(user3); + let _t3 = game.participate(b"user3 randomness", &clock, ts.ctx()); + // User4 buys a ticket + ts.next_tx(user4); + let _t4 = game.participate(b"user4 randomness", &clock, ts.ctx()); + + // Increment time to after submission phase has ended + clock.increment_for_testing(1000); + + // User3 completes by submitting output and proof of the VDF + ts.next_tx(user3); + game.complete(OUTPUT, BAD_PROOF, &clock); + abort 0 + } + + #[test] + #[expected_failure(abort_code = vdf::lottery::EInvalidRandomness)] + fun test_empty_randomness() { + let user1 = @0x0; + + let mut ts = ts::begin(user1); + let clock = clock::create_for_testing(ts.ctx()); + + lottery::create(1000, 1000, &clock, ts.ctx()); + ts.next_tx(user1); + let mut game: Game = ts.take_shared(); + + // User1 buys a ticket, but with wrong randomness length. + ts.next_tx(user1); + let _t = game.participate(b"abcd", &clock, ts.ctx()); + abort 0 + } +}