From 6aaabff522044ee937a0e74230639c3d3eef1558 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Tue, 1 Apr 2025 00:34:08 +0200 Subject: [PATCH] Initial, based on PBC Example Contracts --- .gitignore | 13 + README.md | 0 contract-java-test/.coverage-allowlist.txt | 19 + contract-java-test/pom.xml | 206 +++++++ run-java-tests.sh | 76 +++ rust/Cargo.toml | 54 ++ rust/mia-game/Cargo.toml | 31 + rust/mia-game/README.md | 43 ++ rust/mia-game/src/lib.rs | 572 ++++++++++++++++++ rust/mia-game/src/zk_compute.rs | 44 ++ rust/notamon-asset-contract/Cargo.toml | 29 + rust/notamon-asset-contract/README.md | 0 rust/notamon-asset-contract/src/lib.rs | 106 ++++ rust/notamon-asset-contract/src/zk_compute.rs | 1 + rust/upgradable-v1/Cargo.toml | 25 + rust/upgradable-v1/README.md | 17 + rust/upgradable-v1/src/lib.rs | 51 ++ 17 files changed, 1287 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 contract-java-test/.coverage-allowlist.txt create mode 100644 contract-java-test/pom.xml create mode 100755 run-java-tests.sh create mode 100644 rust/Cargo.toml create mode 100644 rust/mia-game/Cargo.toml create mode 100644 rust/mia-game/README.md create mode 100644 rust/mia-game/src/lib.rs create mode 100644 rust/mia-game/src/zk_compute.rs create mode 100644 rust/notamon-asset-contract/Cargo.toml create mode 100644 rust/notamon-asset-contract/README.md create mode 100644 rust/notamon-asset-contract/src/lib.rs create mode 100644 rust/notamon-asset-contract/src/zk_compute.rs create mode 100644 rust/upgradable-v1/Cargo.toml create mode 100644 rust/upgradable-v1/README.md create mode 100644 rust/upgradable-v1/src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65ea75e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Rust / Cargo +Cargo.lock +target/ + +# Expanded rust +expanded.rs + +.idea/ + +# Maven with auto-import, exclude module files +*.iml + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/contract-java-test/.coverage-allowlist.txt b/contract-java-test/.coverage-allowlist.txt new file mode 100644 index 0000000..3f7b8cb --- /dev/null +++ b/contract-java-test/.coverage-allowlist.txt @@ -0,0 +1,19 @@ +mia-game/src/lib.rs +mia-game/src/zk_compute.rs +upgradable-v1/src/lib.rs # junit does not support coverage for upgrade invocations +upgradable-v2/src/lib.rs # junit does not support coverage for upgrade invocations +upgradable-v2/src/upgrade_from.rs # junit does not support coverage for upgrade invocations +upgradable-v3/src/lib.rs # junit does not support coverage for upgrade invocations +upgradable-v3/src/upgrade_from.rs # junit does not support coverage for upgrade invocations +zk-average-salary/src/contract.rs +zk-average-salary/src/zk_compute.rs +zk-multi-functional/src/zk_compute.rs +zk-second-price-auction/src/contract.rs +zk-second-price-auction/src/zk_compute.rs +zk-statistics/src/zk_compute.rs +zk-struct-open/src/lib.rs +zk-struct-open/src/zk_compute.rs +zk-voting-simple/src/contract.rs +zk-voting-simple/src/zk_compute.rs +zk-classification/src/lib.rs +zk-classification/src/zk_compute.rs diff --git a/contract-java-test/pom.xml b/contract-java-test/pom.xml new file mode 100644 index 0000000..2ef1e73 --- /dev/null +++ b/contract-java-test/pom.xml @@ -0,0 +1,206 @@ + + + 4.0.0 + + com.partisiablockchain.language + example-contracts-java-test + 4.18.0 + + + + AGPL-3.0-or-later + https://www.gnu.org/licenses/agpl.txt + repo + + + + + + gitlab-partisiablockchain + https://gitlab.com/api/v4/groups/12499775/-/packages/maven/ + + + + + gitlab-partisiablockchain + https://gitlab.com/api/v4/groups/12499775/-/packages/maven/ + + + + + com.partisiablockchain + pom + 4.116.0 + + + + com.partisiablockchain.governance:sys-binder + com.partisiablockchain.language:real-wasm-binder + com.partisiablockchain.real:deploy + com.partisiablockchain.language:pub-wasm-binder + com.partisiablockchain.governance:pubdeploy + com.partisiablockchain.governance:routing + com.partisiablockchain.consensus.fasttrack:plugin + com.partisiablockchain.governance:fee + com.partisiablockchain.governance:shared-object-store + + + + + com.partisiablockchain.language + codegen-lib + 5.147.0 + test + + + com.partisiablockchain.governance + sys-binder + 5.44.0 + test + + + com.partisiablockchain.governance + fee + 5.64.0 + test + + + com.partisiablockchain.governance + pubdeploy + 5.183.0 + test + + + com.partisiablockchain.governance + routing + 5.52.0 + test + + + com.partisiablockchain.language + pub-wasm-binder + 5.55.0 + test + + + com.partisiablockchain.language + real-wasm-binder + 5.64.0 + test + + + com.partisiablockchain.real + deploy + 5.189.0 + test + + + com.partisiablockchain.language + junit-contract-test + 5.264.0 + test + + + com.partisiablockchain.consensus.fasttrack + plugin + 5.53.0 + test + + + com.partisiablockchain.governance + shared-object-store + 5.55.0 + test + + + org.assertj + assertj-core + test + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + generate-test-sources + + add-test-source + + + + ${project.basedir}/target/generated-test-sources/generated-abi + + + + + + + com.partisiablockchain.language + abi-generation-maven-plugin + 5.147.0 + + + generate-test-sources + + abi-code-gen + + + true + ${project.basedir}/../rust/target/wasm32-unknown-unknown/release/ + true + + Model + InternalVertex + LeafVertex + Sample + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + generate-test-resources + + write-project-properties + + + ${project.build.testOutputDirectory}/maven.properties + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.5.0 + + + copy + generate-test-resources + + list + + + ${project.build.testOutputDirectory}/maven.dependencies + true + true + + + + + + + + diff --git a/run-java-tests.sh b/run-java-tests.sh new file mode 100755 index 0000000..65d153c --- /dev/null +++ b/run-java-tests.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +compile_contracts() { + pushd rust 1> /dev/null || exit + if [ "$coverage" = true ]; then + cargo partisia-contract build --release --coverage + else + cargo partisia-contract build --release + fi + popd 1> /dev/null || exit +} + +run_java_tests() { + if [ "$coverage" = true ]; then + test_with_coverage + else + test_without_coverage + fi +} + +merge_and_report() { + pushd contract-java-test 1> /dev/null || exit + + # Determine profraw files + find ./target/coverage/profraw/ -type f -name '*.profraw' > ./target/coverage/all-profraw-files + + # Merge profraw + rust-profdata merge -sparse --input-files=./target/coverage/all-profraw-files --output=target/coverage/java_test.profdata + + # Generate report + find ../rust/target/wasm32-unknown-unknown/release/ -type f -executable -print | + sed "s/^/--object /" | + xargs rust-cov show --ignore-filename-regex=".cargo\.*" --ignore-filename-regex="target\.*" \ + --instr-profile=target/coverage/java_test.profdata --Xdemangler=rustfilt --format="html" \ + --output-dir=target/coverage/html + popd 1> /dev/null || exit +} + +test_with_coverage() { + # Run contract tests + pushd contract-java-test 1> /dev/null || exit + mvn test -Dcoverage + popd 1> /dev/null || exit + + merge_and_report +} + +test_without_coverage() { + # Run contract tests + pushd contract-java-test 1> /dev/null || exit + mvn test + popd 1> /dev/null || exit +} + +help() { + echo "usage: ./run-java-tests.sh [-b][-c][-h]" + echo "-b Build the contracts before running tests (if coverage is enabled also generates the instrumented executables)" + echo "-c Test with coverage enabled" + echo "-h Print this help message" + exit 0 +} + +while getopts :bch flag; do + case "${flag}" in + b) build=true ;; + c) coverage=true ;; + h) help ;; + *) echo "Invalid option: -$flag." && help ;; + esac +done + +if [ "$build" = true ]; then + compile_contracts +fi + +run_java_tests diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..77c88d4 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,54 @@ +[workspace] +resolver = "2" + +members = [ + "voting", + "multi-voting", + "petition", + "ping", + "mia-game", + "nickname", + "access-control", + "dns", + "dns-voting-client", + "zk-second-price-auction", + "zk-struct-open", + "zk-immediate-open", + "zk-voting-simple", + "zk-average-salary", + "zk-statistics", + "zk-multi-functional", + "zk-file-share", + "zk-classification", + "upgradable-v1", + "upgradable-v2", + "upgradable-v3", +] + +[workspace.package] +version = "5.203.0" +description = "Example contract for the Partisia Blockchain." +homepage = "https://gitlab.com/partisiablockchain/language/example-contracts" +repository = "https://gitlab.com/partisiablockchain/language/example-contracts" +documentation = "https://gitlab.com/partisiablockchain/language/example-contracts" +edition = "2021" +license = "MIT" + +[workspace.metadata.partisiablockchain] +cargo-partisia = "5.155.0" + +[workspace.metadata.zkcompiler] +url = "https://gitlab.com/api/v4/groups/12499775/-/packages/maven/com/partisiablockchain/language/zkcompiler/5.61.0/zkcompiler-5.61.0-jar-with-dependencies.jar" + +[workspace.metadata.abi-cli] +url = "https://gitlab.com/api/v4/groups/12499775/-/packages/maven/com/partisiablockchain/language/abi-cli/5.147.0/abi-cli-5.147.0-jar-with-dependencies.jar" + +[workspace.dependencies] +pbc_contract_common = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +pbc_contract_codegen = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +pbc_traits = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +pbc_lib = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +read_write_rpc_derive = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +read_write_state_derive = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +create_type_spec_derive = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } +pbc_zk = { git = "https://git@gitlab.com/partisiablockchain/language/contract-sdk.git", tag = "v.16.79.0" } diff --git a/rust/mia-game/Cargo.toml b/rust/mia-game/Cargo.toml new file mode 100644 index 0000000..0916d8f --- /dev/null +++ b/rust/mia-game/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "mia-game" +readme = "README.md" +version.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[features] +abi = ["pbc_contract_common/abi", "pbc_contract_codegen/abi", "pbc_traits/abi", "create_type_spec_derive/abi", "pbc_lib/abi", "pbc_zk/abi"] +plus_metadata = [] + +[lib] +path = "src/lib.rs" +crate-type = ['rlib', 'cdylib'] + +[package.metadata.zk] +zk-compute-path = "src/zk_compute.rs" + +[dependencies] +pbc_contract_common.workspace = true +pbc_traits.workspace = true +pbc_lib.workspace = true +read_write_rpc_derive.workspace = true +read_write_state_derive.workspace = true +create_type_spec_derive.workspace = true +pbc_contract_codegen.workspace = true +pbc_zk.workspace = true diff --git a/rust/mia-game/README.md b/rust/mia-game/README.md new file mode 100644 index 0000000..c5c6db6 --- /dev/null +++ b/rust/mia-game/README.md @@ -0,0 +1,43 @@ +# Mia Gaming Contract + +## Rules + +3 or more players, all start with 6 lives. + +The complete order of rolls (from highest to lowest): + +21 (Mia), 31 (Little Mia), 66, 55, 44, 33, 22, 11, 65, 64, 63, 62, 61, 54, 53, 52, 51, 43, 42, 41, 32 + +### Order of actions + +The first player rolls the dice and keeps their value concealed from the other players. + +The player then has three choices: + +- Tell the truth and announce what has been rolled. +- Lie and announce a greater value than that rolled. +- Lie and announce a lesser value. + +The concealed dice are then passed to the next player in a clockwise fashion. + +The receiving player now has two options: + +- Believe the passer, roll the dice and pass it on, announcing a higher value—with or without looking at them. +- Call the passer a liar and look at the dice. + If the dice show a lesser value than that announced, the passer loses a life and the receiving player starts a new round. + However, if the dice show a greater or equal value, the current player loses a life and the next player starts a new round. + +**Note:** +Each player must always announce a value greater than or equal to the previous value announced. + +##### Mia announced + +If Mia is announced, the next player has two choices: + +- They may give up without looking at the dice and lose one life. +- They may look at the dice. If it was a Mia, they lose two lives. + If it wasn't, the previous player loses two lives. + +### Winning the game + +Last remaining player is the winner. diff --git a/rust/mia-game/src/lib.rs b/rust/mia-game/src/lib.rs new file mode 100644 index 0000000..4783d7c --- /dev/null +++ b/rust/mia-game/src/lib.rs @@ -0,0 +1,572 @@ +#![doc = include_str!("../README.md")] +#![allow(unused_variables)] + +#[macro_use] +extern crate pbc_contract_codegen; +extern crate pbc_contract_common; +extern crate pbc_lib; + +mod zk_compute; + +use create_type_spec_derive::CreateTypeSpec; +use pbc_contract_common::address::Address; +use pbc_contract_common::context::ContractContext; +use pbc_contract_common::events::EventGroup; +use pbc_contract_common::sorted_vec_map::{SortedVecMap, SortedVecSet}; +use pbc_contract_common::zk::{SecretVarId, ZkInputDef, ZkState, ZkStateChange}; +use pbc_traits::ReadWriteState; +use pbc_zk::{Sbi8, SecretBinary}; +use read_write_rpc_derive::ReadWriteRPC; +use read_write_state_derive::ReadWriteState; + +/** + * Metadata information associated with each individual variable. + */ +#[derive(ReadWriteState, ReadWriteRPC, Debug)] +#[repr(u8)] +pub enum SecretVarType { + #[discriminant(0)] + /// Randomness used for dice throws. + Randomness {}, + #[discriminant(1)] + /// Result of dice throws. + ThrowResult {}, +} + +/// The state of the Mia game, which is persisted on-chain. +#[state] +pub struct MiaState { + // The current amount of randomness contributions received for a dice throw. + nr_of_randomness_contributions: u32, + // The number of players at the start of the game. + nr_of_players_at_the_start: u32, + // The player currently throwing the dice and declaring a value. + player_throwing: u32, + // The current phase the game is in, to determine allowed actions. + game_phase: GamePhase, + // The players at the start of the game. + starting_players: Vec
, + // The current players active in the game. + players: Vec
, + // The remaining lives of the players in the game. + player_lives: SortedVecMap, + // The last throw's secret variable id. + throw_result_id: Option, + // The stated value of the current throw. + stated_throw: Option, + // The revealed value of a throw. + throw_result: Option, + // The announced throw value, where the next announced throw must be higher than, to be eligible. + throw_to_beat: DiceThrow, + // The winner of the game. + winner: Option
, +} + +impl MiaState { + /// Get the current player in turn. + fn current_player(&self) -> &Address { + &self.players[self.player_throwing as usize] + } + /// Get the next player in turn. + fn next_player(&self) -> &Address { + &self.players[(self.player_throwing + 1) as usize % self.players.len()] + } + + /// Replace the current player in turn with the next player. + fn go_to_next_player(&mut self) { + self.player_throwing = (self.player_throwing + 1) % self.players.len() as u32; + } + + /// Check whether a player is dead i.e. have no lives left. + fn is_player_dead(&self, player: Address) -> bool { + self.player_lives[&player] == 0 + } + + /// Remove a dead player from the list of players. + fn remove_dead_player(&mut self, player: Address) { + self.players.retain(|p| player != *p); + } + + /// Reduce a players lives by a given integer. + fn reduce_players_life_by(&mut self, player: Address, lives_lost: u8) { + if self.player_lives[&player] >= lives_lost { + self.player_lives + .insert(player, self.player_lives[&player] - lives_lost); + } else { + self.player_lives.insert(player, 0); + } + } + + /// Check whether the game if finished, i.e. whether only one player remains. + fn is_the_game_finished(&self) -> bool { + self.players.len() == 1 + } + + /// Get the last remaining player, the winner. + fn get_winner(&self) -> Address { + *self.players.first().unwrap() + } +} + +/// A throw of two dice. +#[derive(ReadWriteState, ReadWriteRPC, CreateTypeSpec, Debug, Copy, Clone)] +pub struct DiceThrow { + d1: u8, + d2: u8, +} + +impl DiceThrow { + /// The value of each die is reduced to be between 0 and 5. + fn reduce(&self) -> DiceThrow { + DiceThrow { + d1: self.d1 % 6, + d2: self.d2 % 6, + } + } + + /// Checks whether a throw is better than the current dice throw to beat. + /// The dice throws are compared based on their associated values. + fn better_than_or_equal(self, actual: DiceThrow) -> bool { + self.get_throw_score() >= actual.get_throw_score() + } + + /// Checks whether a dice throw is Mia, i.e. is (0,1) or (1,0). + fn is_mia(self) -> bool { + (self.d1 == 0 && self.d2 == 1) || (self.d2 == 0 && self.d1 == 1) + } + + /// Checks whether a dice throw is Little Mia, i.e. is (0,2) or (2,0). + fn is_little_mia(self) -> bool { + (self.d1 == 0 && self.d2 == 2) || (self.d2 == 0 && self.d1 == 2) + } + + /// Checks whether both dices in the dice throw have the same value. + fn is_pair(self) -> bool { + self.d1 == self.d2 + } + + /// Get the score of a dice throw. + /// The throw values are determined such that the highest roll is Mia, then Little Mia, + /// followed by the doubles from (5,5) to (0,0), and then all other rolls from (5,4) + /// down to (2,1). + fn get_throw_score(self) -> u8 { + let mut value = 0; + if self.is_mia() { + value += 128; + }; + if self.is_little_mia() { + value += 64; + }; + if self.is_pair() { + value += 32; + }; + if (self.d1 == 5) || (self.d2 == 5) { + value += 16; + } + if (self.d1 == 4) || (self.d2 == 4) { + value += 8; + } + if (self.d1 == 3) || (self.d2 == 3) { + value += 4; + } + if (self.d1 == 2) || (self.d2 == 2) { + value += 2; + } + if (self.d1 == 1) || (self.d2 == 1) { + value += 1; + } + value + } +} + +/// The contribution each player must send to make a dice throw. The contributions should be in the +/// interval \[ 0, 5 \] inclusive. If the contributions are outside this interval, +/// they are normalized to the interval. +#[derive(CreateTypeSpec, SecretBinary)] +pub struct RandomContribution { + d1: Sbi8, + d2: Sbi8, +} + +/// The different phases the contract can be in before, during and after a game of Mia. +#[derive(ReadWriteRPC, ReadWriteState, CreateTypeSpec, Debug, PartialEq, Copy, Clone)] +pub enum GamePhase { + #[discriminant(0)] + /// The game has been initialized. + Start {}, + #[discriminant(1)] + /// Players can add randomness as secret inputs. + AddRandomness {}, + #[discriminant(2)] + /// The player in turn can throw the dice. + Throw {}, + #[discriminant(3)] + /// The player in turn can announce their throw. + Announce {}, + #[discriminant(4)] + /// The next player in turn can believe or call out the player's announced throw. + Decide {}, + #[discriminant(5)] + /// If a player was called out, they can reveal their actual throw. + Reveal {}, + #[discriminant(6)] + /// The game is finished. + Done {}, +} + +/// Initialize a new mia game. +/// +/// # Arguments +/// +/// * `_ctx` - the contract context containing information about the sender and the blockchain. +/// * +/// +/// # Returns +/// +/// The initial state of the petition, with no signers. +/// +#[init(zk = true)] +pub fn initialize( + context: ContractContext, + zk_state: ZkState, + addresses_to_play: Vec
, +) -> (MiaState, Vec) { + assert!( + addresses_to_play.len() >= 3, + "There must be at least 3 players to play Mia." + ); + assert_eq!( + SortedVecSet::from(addresses_to_play.clone()).len(), + addresses_to_play.len(), + "No duplicates in players." + ); + + let mut state = MiaState { + starting_players: addresses_to_play.clone(), + players: addresses_to_play.clone(), + nr_of_players_at_the_start: addresses_to_play.len() as u32, + player_lives: SortedVecMap::new(), + game_phase: GamePhase::Start {}, + player_throwing: 0, + nr_of_randomness_contributions: 0, + throw_result_id: None, + stated_throw: None, + throw_result: None, + winner: None, + throw_to_beat: DiceThrow { d1: 1, d2: 2 }, + }; + + for address in addresses_to_play { + state.player_lives.insert(address, 6); + } + + (state, vec![]) +} + +/// Start the game. +/// +/// # Arguments +/// +/// * `ctx` - the contract context containing information about the sender and the blockchain. +/// * `state` - the current state of the game. +/// +/// # Returns +/// +/// The updated vote state reflecting the new signing. +/// +#[action(shortname = 0x01, zk = true)] +pub fn start_round( + context: ContractContext, + mut state: MiaState, + zk_state: ZkState, +) -> (MiaState, Vec, Vec) { + assert_eq!( + state.players[state.player_throwing as usize], context.sender, + "Only the player whose turn it is can start the round." + ); + state.game_phase = GamePhase::AddRandomness {}; + + (state, vec![], vec![]) +} + +/// Add randomness for the next dice throw. +/// The sender must be a player in the game to add randomness. +#[zk_on_secret_input(shortname = 0x40, secret_type = "RandomContribution")] +pub fn add_randomness_to_throw( + context: ContractContext, + state: MiaState, + zk_state: ZkState, +) -> ( + MiaState, + Vec, + ZkInputDef, +) { + assert_eq!( + state.game_phase, + GamePhase::AddRandomness {}, + "Must be in the AddRandomness phase to input secret randomness." + ); + assert!(state.starting_players.contains(&context.sender)); + assert!( + zk_state + .secret_variables + .iter() + .chain(zk_state.pending_inputs.iter()) + .all(|(_, secret_variable)| secret_variable.owner != context.sender), + "Each Player is only allowed to send one contribution to the randomness of the dice throw. Sender: {:?}", + context.sender + ); + + let input_def = ZkInputDef::with_metadata( + Some(SHORTNAME_INPUTTED_VARIABLE), + SecretVarType::Randomness {}, + ); + + (state, vec![], input_def) +} + +/// Automatically called when a variable is confirmed on chain. +/// +/// Initializes opening. +#[zk_on_variable_inputted(shortname = 0x01)] +fn inputted_variable( + context: ContractContext, + mut state: MiaState, + zk_state: ZkState, + variable_id: SecretVarId, +) -> MiaState { + if state.nr_of_randomness_contributions == state.nr_of_players_at_the_start - 1 { + state.nr_of_randomness_contributions = 0; + state.game_phase = GamePhase::Throw {}; + } else { + state.nr_of_randomness_contributions += 1; + } + state +} + +/// Start the computation to compute the dice throw. +#[action(shortname = 0x02, zk = true)] +pub fn throw_dice( + context: ContractContext, + state: MiaState, + zk_state: ZkState, +) -> (MiaState, Vec, Vec) { + assert_eq!( + state.game_phase, + GamePhase::Throw {}, + "The dice can only be thrown in the Throw phase" + ); + assert_eq!( + *state.current_player(), + context.sender, + "Only the player in turn can throw the dice. It is currently {:?}s turn", + state.current_player() + ); + + ( + state, + vec![], + vec![zk_compute::compute_dice_throw_start( + Some(SHORTNAME_SUM_COMPUTE_COMPLETE), + &SecretVarType::ThrowResult {}, + )], + ) +} + +/// Automatically called when the sum of the random contributions are done. +/// Transfers the resulting throw to the player throwing the dice. +#[zk_on_compute_complete(shortname = 0x01)] +fn sum_compute_complete( + _context: ContractContext, + mut state: MiaState, + zk_state: ZkState, + output_variables: Vec, +) -> (MiaState, Vec, Vec) { + let Some(result_id) = output_variables.first() else { + panic!("No result") + }; + + state.throw_result_id = Some(*result_id); + state.game_phase = GamePhase::Announce {}; + let player_to_transfer_to = *state.current_player(); + + ( + state, + vec![], + vec![ + ZkStateChange::TransferVariable { + variable: *result_id, + new_owner: player_to_transfer_to, + }, + ZkStateChange::DeleteVariables { + variables_to_delete: zk_state + .secret_variables + .iter() + .map(|(variable_id, _)| variable_id) + .filter(|id| id != result_id) + .collect(), + }, + ], + ) +} + +/// Announce a value such that the next player can decide if they believe it or not. +/// The value must be higher than or equal to the throw to beat. +#[action(shortname = 0x03, zk = true)] +fn announce_throw( + context: ContractContext, + mut state: MiaState, + zk_state: ZkState, + dice_value: DiceThrow, +) -> (MiaState, Vec, Vec) { + assert_eq!( + *state.current_player(), + context.sender, + "Only the current player can state the value of the dice throw." + ); + + let reduced_dice_value = dice_value.reduce(); + + if !reduced_dice_value.better_than_or_equal(state.throw_to_beat) { + panic!("Stated throw must be better than the last stated throw.") + } + + state.stated_throw = Some(dice_value); + state.game_phase = GamePhase::Decide {}; + + (state, vec![], vec![]) +} + +/// The next player believes the stated throw, and continues the round, where the throw to beat is +/// the stated throw. +#[action(shortname = 0x04, zk = true)] +fn believe( + context: ContractContext, + mut state: MiaState, + zk_state: ZkState, +) -> (MiaState, Vec, Vec) { + assert_eq!( + context.sender, + *state.next_player(), + "Only the next player can say if they believe the stated throw." + ); + assert_eq!( + state.game_phase, + GamePhase::Decide {}, + "Must be in the deciding phase to say believe." + ); + + state.game_phase = GamePhase::AddRandomness {}; + state.throw_to_beat = state.stated_throw.unwrap(); + state.stated_throw = None; + state.go_to_next_player(); + + ( + state, + vec![], + vec![ZkStateChange::DeleteVariables { + variables_to_delete: zk_state + .secret_variables + .iter() + .map(|(variable_id, _)| variable_id) + .collect(), + }], + ) +} + +/// The next player does not believe the stated throw, and starts a reveal of the dice. +#[action(shortname = 0x05, zk = true)] +fn call_out( + context: ContractContext, + mut state: MiaState, + zk_state: ZkState, +) -> (MiaState, Vec, Vec) { + assert_eq!( + context.sender, + *state.next_player(), + "Only the next player can say if the throwing player is lying." + ); + assert_eq!( + state.game_phase, + GamePhase::Decide {}, + "Must be in the deciding phase say if the throwing player is lying." + ); + let variable_to_open = state.throw_result_id.unwrap(); + state.game_phase = GamePhase::Reveal {}; + ( + state, + vec![], + vec![ZkStateChange::OpenVariables { + variables: vec![variable_to_open], + }], + ) +} + +/// Saves the opened variable in state and readies another computation. +#[zk_on_variables_opened] +fn save_opened_variable( + context: ContractContext, + mut state: MiaState, + zk_state: ZkState, + opened_variables: Vec, +) -> (MiaState, Vec, Vec) { + assert_eq!( + opened_variables.len(), + 1, + "Can only show one set of dice at a time." + ); + + let variable_id = opened_variables.first().unwrap(); + let result: DiceThrow = read_opened_variable_data(&zk_state, variable_id).unwrap(); + + let result_reduced = result.reduce(); + + let Some(stated_throw) = state.stated_throw else { + panic!("Could not find a stated throw in state.") + }; + + let stated_throw_reduced = stated_throw.reduce(); + + let loser_of_round = if result.better_than_or_equal(stated_throw_reduced) { + *state.next_player() + } else { + *state.current_player() + }; + + if stated_throw.is_mia() { + state.reduce_players_life_by(loser_of_round, 2); + } else { + state.reduce_players_life_by(loser_of_round, 1); + } + + if state.is_player_dead(loser_of_round) { + state.remove_dead_player(loser_of_round); + } + + state.throw_result = Some(result_reduced); + + if state.is_the_game_finished() { + state.game_phase = GamePhase::Done {}; + state.winner = Some(state.get_winner()); + } else { + state.go_to_next_player(); + state.game_phase = GamePhase::AddRandomness {}; + } + + ( + state, + vec![], + vec![ZkStateChange::DeleteVariables { + variables_to_delete: opened_variables, + }], + ) +} + +/// Reads the data from a revealed secret variable +fn read_opened_variable_data( + zk_state: &ZkState, + variable_id: &SecretVarId, +) -> Option { + let variable = zk_state.get_variable(*variable_id)?; + variable.open_value() +} diff --git a/rust/mia-game/src/zk_compute.rs b/rust/mia-game/src/zk_compute.rs new file mode 100644 index 0000000..3ed9087 --- /dev/null +++ b/rust/mia-game/src/zk_compute.rs @@ -0,0 +1,44 @@ +use pbc_zk::*; + +/// Output variable type +#[derive(pbc_zk::SecretBinary, Clone)] +struct RandomnessInput { + /// Token amount. + d1: Sbi8, + d2: Sbi8, +} + +/// Perform a zk computation on secret-shared randomness added to make a random dice throw. +/// +/// ### Returns: +/// +/// The sum of the randomness contributions variables. +#[zk_compute(shortname = 0x61)] +pub fn compute_dice_throw() -> RandomnessInput { + let mut throw = RandomnessInput { + d1: Sbi8::from(0), + d2: Sbi8::from(0), + }; + + for variable_id in secret_variable_ids() { + let mut raw_contribution: RandomnessInput = load_sbi::(variable_id); + + let d1_reduced = reduce_contribution(raw_contribution.d1); + let d2_reduced = reduce_contribution(raw_contribution.d2); + + throw.d1 = throw.d1 + d1_reduced; + throw.d2 = throw.d2 + d2_reduced; + } + + throw +} + +/// Reduce the contribution if it is not between 0 and 5. +fn reduce_contribution(value: Sbi8) -> Sbi8 { + let reduce = value & Sbi8::from(0b111); + if reduce >= Sbi8::from(6) { + reduce - Sbi8::from(6) + } else { + reduce + } +} diff --git a/rust/notamon-asset-contract/Cargo.toml b/rust/notamon-asset-contract/Cargo.toml new file mode 100644 index 0000000..55b4dd1 --- /dev/null +++ b/rust/notamon-asset-contract/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "zk-file-share" +readme = "README.md" +version.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[features] +abi = ["pbc_contract_common/abi", "pbc_contract_codegen/abi", "pbc_traits/abi", "create_type_spec_derive/abi", "pbc_lib/abi"] + +[lib] +crate-type = ['rlib', 'cdylib'] + +[package.metadata.zk] +zk-compute-path = "src/zk_compute.rs" + +[dependencies] +pbc_contract_common.workspace = true +pbc_traits.workspace = true +pbc_lib.workspace = true +read_write_rpc_derive.workspace = true +read_write_state_derive.workspace = true +create_type_spec_derive.workspace = true +pbc_contract_codegen.workspace = true +pbc_zk.workspace = true diff --git a/rust/notamon-asset-contract/README.md b/rust/notamon-asset-contract/README.md new file mode 100644 index 0000000..e69de29 diff --git a/rust/notamon-asset-contract/src/lib.rs b/rust/notamon-asset-contract/src/lib.rs new file mode 100644 index 0000000..ce8d438 --- /dev/null +++ b/rust/notamon-asset-contract/src/lib.rs @@ -0,0 +1,106 @@ +#![doc = include_str!("../README.md")] +#![allow(unused_variables)] + +#[macro_use] +extern crate pbc_contract_codegen; +extern crate pbc_contract_common; +extern crate pbc_lib; + +use pbc_contract_common::address::Address; +use pbc_contract_common::context::ContractContext; +use pbc_contract_common::events::EventGroup; +use pbc_contract_common::zk::{SecretVarId, ZkInputDef, ZkState, ZkStateChange}; +use pbc_zk::Sbi8; +use read_write_rpc_derive::ReadWriteRPC; +use read_write_state_derive::ReadWriteState; + +mod zk_compute; + +/// Metadata for secret-shared files. +#[derive(ReadWriteState, ReadWriteRPC, Debug)] +#[repr(C)] +pub struct SecretVarMetadata {} + +/// Empty contract state, as all stored files are secret-shared. +#[state] +pub struct CollectionState {} + +/// Initializes contract with empty state. +#[init(zk = true)] +pub fn initialize(ctx: ContractContext, zk_state: ZkState) -> CollectionState { + CollectionState {} +} + +/// Upload a new file with a specific size of `file_length`. +/// +/// `file_length` is the size of the file in *bytes*. +/// Fails if the uploaded file has a different size than `file_length`. +#[zk_on_secret_input(shortname = 0x42)] +pub fn add_file( + context: ContractContext, + state: CollectionState, + zk_state: ZkState, + file_length: u32, +) -> ( + CollectionState, + Vec, + ZkInputDef>, +) { + let input_def = ZkInputDef::with_metadata_and_size(None, SecretVarMetadata {}, file_length * 8); + (state, vec![], input_def) +} + +/// Changes ownership of the secret-shared file with id `file_id` +/// from the sender to `new_owner`. +/// +/// Fails if the sender is not the current owner of the referenced file. +#[action(shortname = 0x03, zk = true)] +pub fn change_file_owner( + ctx: ContractContext, + state: CollectionState, + zk_state: ZkState, + file_id: u32, + new_owner: Address, +) -> (CollectionState, Vec, Vec) { + let file_id = SecretVarId::new(file_id); + let file_owner = zk_state.get_variable(file_id).unwrap().owner; + assert_eq!( + file_owner, ctx.sender, + "Only the owner of the secret file is allowed to change ownership." + ); + + ( + state, + vec![], + vec![ZkStateChange::TransferVariable { + variable: file_id, + new_owner, + }], + ) +} + +/// Deletes the secret-shared file with id `file_id`. +/// +/// Fails if the sender is not the current owner of the secret file. +#[action(shortname = 0x05, zk = true)] +pub fn delete_file( + ctx: ContractContext, + state: CollectionState, + zk_state: ZkState, + file_id: u32, +) -> (CollectionState, Vec, Vec) { + let file_id = SecretVarId::new(file_id); + let file_owner = zk_state.get_variable(file_id).unwrap().owner; + assert_eq!( + file_owner, ctx.sender, + "Only the owner of the secret file is allowed to delete it." + ); + + ( + state, + vec![], + vec![ZkStateChange::DeleteVariables { + variables_to_delete: vec![file_id], + }], + ) +} diff --git a/rust/notamon-asset-contract/src/zk_compute.rs b/rust/notamon-asset-contract/src/zk_compute.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rust/notamon-asset-contract/src/zk_compute.rs @@ -0,0 +1 @@ + diff --git a/rust/upgradable-v1/Cargo.toml b/rust/upgradable-v1/Cargo.toml new file mode 100644 index 0000000..ee5f09b --- /dev/null +++ b/rust/upgradable-v1/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "upgradable-v1" +readme = "README.md" +version.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ['rlib', 'cdylib'] + +[dependencies] +pbc_contract_common.workspace = true +pbc_traits.workspace = true +pbc_lib.workspace = true +read_write_rpc_derive.workspace = true +read_write_state_derive.workspace = true +create_type_spec_derive.workspace = true +pbc_contract_codegen.workspace = true + +[features] +abi = ["pbc_contract_common/abi", "pbc_contract_codegen/abi", "pbc_traits/abi", "create_type_spec_derive/abi", "pbc_lib/abi"] diff --git a/rust/upgradable-v1/README.md b/rust/upgradable-v1/README.md new file mode 100644 index 0000000..a7d191d --- /dev/null +++ b/rust/upgradable-v1/README.md @@ -0,0 +1,17 @@ +# Upgradable v1 + +The simplest possible upgradable example contract that retains some amount of +security and usability. + +The [`UpgradableV1State`] contains the address of the account or contract that is +allowed to upgrade this contract. + +Contract can only be upgraded to a different contract, it cannot be upgraded to +itself, or from any other kind of contract. + +## About upgrade governance + +This contract is an example, and does not reflect what good upgrade logic for a +contract should look like. Please read documentation page for [upgradable smart +contracts](https://partisiablockchain.gitlab.io/documentation/smart-contracts/upgradable-smart-contracts.html) +for suggestion of how to implement the upgrade governance. diff --git a/rust/upgradable-v1/src/lib.rs b/rust/upgradable-v1/src/lib.rs new file mode 100644 index 0000000..82b3c03 --- /dev/null +++ b/rust/upgradable-v1/src/lib.rs @@ -0,0 +1,51 @@ +#![doc = include_str!("../README.md")] + +#[macro_use] +extern crate pbc_contract_codegen; +use pbc_contract_codegen::{init, state, upgrade_is_allowed}; + +use pbc_contract_common::address::Address; +use pbc_contract_common::context::ContractContext; +use pbc_contract_common::upgrade::ContractHashes; + +/// Contract state. +#[state] +pub struct ContractState { + /// Contract or account allowed to upgrade this contract. + upgrader: Address, + /// Counter to demonstrate changes in behaviour + counter: u32, +} + +/// Initialize contract with the upgrader address. +#[init] +pub fn initialize(_ctx: ContractContext, upgrader: Address) -> ContractState { + ContractState { + counter: 0, + upgrader, + } +} + +/// Checks whether the upgrade is allowed. +/// +/// This contract allows the [`ContractState::upgrader`] to upgrade the contract at any time. +#[upgrade_is_allowed] +pub fn is_upgrade_allowed( + context: ContractContext, + state: ContractState, + _old_contract_hashes: ContractHashes, + _new_contract_hashes: ContractHashes, + _new_contract_rpc: Vec, +) -> bool { + context.sender == state.upgrader +} + +/// Increment the counter by one. +#[action(shortname = 0x01)] +pub fn increment_counter_by_one( + _context: ContractContext, + mut state: ContractState, +) -> ContractState { + state.counter += 1; + state +}