diff --git a/.github/workflows/all_canisters_test_suite_on_any_push.yml b/.github/workflows/all_canisters_test_suite_on_any_push.yml index c5ffd7ca..5e3e26da 100644 --- a/.github/workflows/all_canisters_test_suite_on_any_push.yml +++ b/.github/workflows/all_canisters_test_suite_on_any_push.yml @@ -49,19 +49,19 @@ jobs: run: nix-shell --run "dfx stop" - name: Build platform_orchestrator canister run: | - nix-shell --run "dfx build platform_orchestrator" + nix-shell --run "dfx build platform_orchestrator --ic" gzip -f -1 ./target/wasm32-unknown-unknown/release/platform_orchestrator.wasm - name: Build individual_user_template canister run: | - nix-shell --run "dfx build individual_user_template" + nix-shell --run "dfx build individual_user_template --ic" gzip -f -1 ./target/wasm32-unknown-unknown/release/individual_user_template.wasm - name: Build user_index canister run: | - nix-shell --run "dfx build user_index" + nix-shell --run "dfx build user_index --ic" gzip -f -1 ./target/wasm32-unknown-unknown/release/user_index.wasm - name: Build post_cache canister run: | - nix-shell --run "dfx build post_cache" + nix-shell --run "dfx build post_cache --ic" gzip -f -1 ./target/wasm32-unknown-unknown/release/post_cache.wasm - name: Run canister test suite env: diff --git a/Cargo.lock b/Cargo.lock index 1bc875d8..d273df8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,12 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.5.2" @@ -324,6 +330,19 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -437,6 +456,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + [[package]] name = "byte-unit" version = "4.0.19" @@ -753,6 +778,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1204,6 +1235,12 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -1593,6 +1630,21 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hash-db" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7d7786361d7425ae2fe4f9e407eb0efaa0840f5212d109cc018c40c35c6ab4" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3375,6 +3427,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -3556,9 +3619,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -3711,6 +3774,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memory-db" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808b50db46293432a45e63bc15ea51e0ab4c0a1647b8eb114e31a3e698dd6fbe" +dependencies = [ + "hash-db", +] + [[package]] name = "merlin" version = "3.0.0" @@ -3729,6 +3801,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3974,6 +4056,17 @@ dependencies = [ "group 0.12.1", ] +[[package]] +name = "parity-scale-codec" +version = "3.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +dependencies = [ + "arrayvec 0.7.6", + "byte-slice-cast", + "impl-trait-for-tuples", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -4156,11 +4249,10 @@ dependencies = [ [[package]] name = "pocket-ic" -version = "3.1.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9765eeff77b8750cf6258eaeea237b96607cd770aa3d4003f021924192b7e4e" +checksum = "beff607d4dbebff8d003453ced669d2645e905de496ca93713f3d47633357e6c" dependencies = [ - "async-trait", "base64 0.13.1", "candid", "hex", @@ -4170,6 +4262,9 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "sha2 0.10.8", + "slog", + "tokio", "tracing", "tracing-appender", "tracing-subscriber", @@ -4662,6 +4757,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -5216,19 +5312,27 @@ dependencies = [ name = "shared_utils" version = "0.1.0" dependencies = [ + "blake3", "candid", "ciborium", + "ed25519-compact", "futures", + "hash-db", + "hash256-std-hasher", "ic-cdk 0.15.1", "ic-cdk-timers", "ic-stable-structures", "icrc-ledger-types 0.1.6", + "memory-db", + "parity-scale-codec", "pprof", "rmp-serde", "serde", "serde_bytes", "serde_json_any_key", "test_utils", + "trie-db", + "trie-root", ] [[package]] @@ -5237,6 +5341,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -5617,11 +5730,25 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -5788,6 +5915,26 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "trie-db" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c992b4f40c234a074d48a757efeabb1a6be88af84c0c23f7ca158950cb0ae7f" +dependencies = [ + "hash-db", + "log", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ed310ef5ab98f5fa467900ed906cb9232dd5376597e00fd4cba2a449d06c0b" +dependencies = [ + "hash-db", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5812,6 +5959,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -5826,9 +5982,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] diff --git a/Cargo.toml b/Cargo.toml index 7155d2fc..9f290772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ [workspace.dependencies] candid = "0.10.2" ciborium = "0.2.1" -pocket-ic = "3.0.0" +pocket-ic = "5.0.0" ic-cdk = "0.15.1" ic-cdk-timers = "0.7.0" ic-cdk-macros = "0.16.0" diff --git a/default.nix b/default.nix index ac65b3a9..100542d4 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ let rev = "1c3a28d84f970e7774af04372ade06399add182e"; nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; pkgs = import nixpkgs { }; - dfx-env = import (builtins.fetchTarball "https://github.com/ninegua/ic-nix/releases/download/20240610/dfx-env.tar.gz") { version = "20240610"; inherit pkgs; }; + dfx-env = import (builtins.fetchTarball "https://github.com/ninegua/ic-nix/releases/download/20240924/dfx-env.tar.gz") { version = "20240924"; inherit pkgs; }; in dfx-env.overrideAttrs (old: { nativeBuildInputs = with pkgs; old.nativeBuildInputs ++ diff --git a/pocket-ic b/pocket-ic index 2e723978..84f59e98 100755 Binary files a/pocket-ic and b/pocket-ic differ diff --git a/scripts/canisters/local_deploy/create_pool_of_individual_canister_user_index.sh b/scripts/canisters/local_deploy/create_pool_of_individual_canister_user_index.sh index 04ddfc79..86caf938 100755 --- a/scripts/canisters/local_deploy/create_pool_of_individual_canister_user_index.sh +++ b/scripts/canisters/local_deploy/create_pool_of_individual_canister_user_index.sh @@ -12,5 +12,5 @@ char_escaped=$(printf "%s" "$char" | sed 's/../\\&/g') # Create a shell script with the escaped hexadecimal string printf "(\"v1.0.0\", blob \"%s\")" "$char_escaped" > argument -dfx canister call user_index create_pool_of_individual_user_available_canisters --argument-file argument dfx ledger fabricate-cycles --cycles 20000000000000000 --canister user_index +dfx canister call user_index create_pool_of_individual_user_available_canisters --argument-file argument diff --git a/scripts/canisters/local_deploy/install_all_canisters.sh b/scripts/canisters/local_deploy/install_all_canisters.sh index 08ce0447..db0069d7 100755 --- a/scripts/canisters/local_deploy/install_all_canisters.sh +++ b/scripts/canisters/local_deploy/install_all_canisters.sh @@ -27,17 +27,8 @@ dfx canister create --no-wallet post_cache dfx canister create --no-wallet user_index dfx canister create --no-wallet platform_orchestrator -gzip_canister() { - gzip -f -1 ./target/wasm32-unknown-unknown/release/$1.wasm -} - scripts/candid_generator.sh -gzip_canister individual_user_template -gzip_canister user_index -gzip_canister post_cache -gzip_canister platform_orchestrator - if [[ $skip_test != true ]] then cargo test @@ -90,6 +81,9 @@ dfx canister install user_index --argument "(record { vec { variant { CanisterAdmin }; variant { CanisterController }; } }; }; + proof_of_participation = opt record { + chain = vec {}; + }; version= \"v1.0.0\" })" diff --git a/src/canister/individual_user_template/Cargo.toml b/src/canister/individual_user_template/Cargo.toml index 8f324188..f3be9a3c 100644 --- a/src/canister/individual_user_template/Cargo.toml +++ b/src/canister/individual_user_template/Cargo.toml @@ -35,3 +35,4 @@ hex = "0.4.3" [dev-dependencies] test_utils = { workspace = true } + diff --git a/src/canister/individual_user_template/can.did b/src/canister/individual_user_template/can.did index c15e507d..36259049 100644 --- a/src/canister/individual_user_template/can.did +++ b/src/canister/individual_user_template/can.did @@ -4,6 +4,10 @@ type AggregateStats = record { total_number_of_hot_bets : nat64; }; type AirdropDistribution = record { airdrop_neurons : vec NeuronDistribution }; +type AirdropMember = record { + user_principal : principal; + user_canister : principal; +}; type BetDetails = record { bet_direction : BetDirection; bet_maker_canister_id : principal; @@ -155,6 +159,7 @@ type IdealMatchedParticipationFunction = record { serialized_representation : opt text; }; type IndividualUserTemplateInitArgs = record { + proof_of_participation : opt ProofOfParticipation; known_principal_ids : opt vec record { KnownPrincipalType; principal }; version : text; url_to_send_canister_metrics_to : opt text; @@ -337,6 +342,13 @@ type PostViewStatistics = record { average_watch_percentage : nat8; threshold_view_count : nat64; }; +type ProofOfChild = record { + proof_of_inclusion : vec blob; + "principal" : principal; + children_proof : ProofOfChildren; +}; +type ProofOfChildren = record { signature : blob; merkle_root : blob }; +type ProofOfParticipation = record { chain : vec ProofOfChild }; type RejectionCode = variant { NoError; CanisterError; @@ -348,44 +360,43 @@ type RejectionCode = variant { }; type Result = variant { Ok : bool; Err : text }; type Result_1 = variant { Ok : nat64; Err : text }; -type Result_10 = variant { Ok : Post; Err }; -type Result_11 = variant { Ok : SystemTime; Err : text }; -type Result_12 = variant { +type Result_10 = variant { Ok : BetDetails; Err : text }; +type Result_11 = variant { Ok : Post; Err }; +type Result_12 = variant { Ok : SystemTime; Err : text }; +type Result_13 = variant { Ok : vec PostDetailsForFrontend; Err : GetPostsOfUserProfileError; }; -type Result_13 = variant { Ok : SessionType; Err : text }; -type Result_14 = variant { Ok : vec SuccessHistoryItemV1; Err : text }; -type Result_15 = variant { Ok : vec principal; Err : PaginationError }; -type Result_16 = variant { +type Result_14 = variant { Ok : SessionType; Err : text }; +type Result_15 = variant { Ok : vec SuccessHistoryItemV1; Err : text }; +type Result_16 = variant { Ok : vec principal; Err : PaginationError }; +type Result_17 = variant { Ok : vec record { nat64; TokenEvent }; Err : PaginationError; }; -type Result_17 = variant { Ok : vec WatchHistoryItem; Err : text }; -type Result_18 = variant { Ok : vec text; Err : NamespaceErrors }; -type Result_19 = variant { Ok : vec record { nat64; nat8 }; Err : text }; +type Result_18 = variant { Ok : vec WatchHistoryItem; Err : text }; +type Result_19 = variant { Ok : vec text; Err : NamespaceErrors }; type Result_2 = variant { Ok : bool; Err : CdaoTokenError }; -type Result_20 = variant { Ok; Err : MigrationErrors }; -type Result_21 = variant { Committed : Committed; Aborted : record {} }; -type Result_22 = variant { Ok : Ok; Err : GovernanceError }; -type Result_23 = variant { Ok; Err : CdaoTokenError }; +type Result_20 = variant { Ok : vec record { nat64; nat8 }; Err : text }; +type Result_21 = variant { Ok; Err : MigrationErrors }; +type Result_22 = variant { Committed : Committed; Aborted : record {} }; +type Result_23 = variant { Ok : Ok; Err : GovernanceError }; type Result_24 = variant { Ok : text; Err : text }; type Result_25 = variant { Ok : UserProfileDetailsForFrontend; Err : UpdateProfileDetailsError; }; -type Result_26 = variant { Ok; Err : text }; -type Result_27 = variant { Ok; Err : UpdateProfileSetUniqueUsernameError }; -type Result_3 = variant { +type Result_26 = variant { Ok; Err : UpdateProfileSetUniqueUsernameError }; +type Result_3 = variant { Ok; Err : text }; +type Result_4 = variant { Ok : BettingStatus; Err : BetOnCurrentlyViewingPostError; }; -type Result_4 = variant { Ok : NamespaceForFrontend; Err : NamespaceErrors }; -type Result_5 = variant { Ok : opt text; Err : NamespaceErrors }; -type Result_6 = variant { Ok; Err : NamespaceErrors }; -type Result_7 = variant { Ok : DeployedCdaoCanisters; Err : CdaoDeployError }; -type Result_8 = variant { Ok : bool; Err : FollowAnotherUserProfileError }; -type Result_9 = variant { Ok : BetDetails; Err : text }; +type Result_5 = variant { Ok : NamespaceForFrontend; Err : NamespaceErrors }; +type Result_6 = variant { Ok : opt text; Err : NamespaceErrors }; +type Result_7 = variant { Ok; Err : NamespaceErrors }; +type Result_8 = variant { Ok : DeployedCdaoCanisters; Err : CdaoDeployError }; +type Result_9 = variant { Ok : bool; Err : FollowAnotherUserProfileError }; type RoomBetPossibleOutcomes = variant { HotWon; BetOngoing; Draw; NotWon }; type RoomDetails = record { total_hot_bets : nat64; @@ -396,10 +407,10 @@ type RoomDetails = record { }; type SessionType = variant { AnonymousSession; RegisteredSession }; type SettleNeuronsFundParticipationRequest = record { - result : opt Result_21; + result : opt Result_22; nns_proposal_id : opt nat64; }; -type SettleNeuronsFundParticipationResponse = record { result : opt Result_22 }; +type SettleNeuronsFundParticipationResponse = record { result : opt Result_23 }; type SlotDetails = record { room_details : vec record { nat64; RoomDetails } }; type SnsInitPayload = record { url : opt text; @@ -548,21 +559,30 @@ service : (IndividualUserTemplateInitArgs) -> { add_device_id : (text) -> (Result); add_post_v2 : (PostDetailsFromFrontend) -> (Result_1); add_token : (principal) -> (Result_2); - bet_on_currently_viewing_post : (PlaceBetArg) -> (Result_3); + add_tokens : (vec principal) -> (vec Result_2); + add_user_to_airdrop_chain : (ProofOfParticipation, AirdropMember) -> ( + Result_3, + ); + bet_on_currently_viewing_post : (PlaceBetArg) -> (Result_4); check_and_update_scores_and_share_with_post_cache_if_difference_beyond_threshold : ( vec nat64, ) -> (); clear_snapshot : () -> (); - create_a_namespace : (text) -> (Result_4); - delete_key_value_pair : (nat64, text) -> (Result_5); - delete_multiple_key_value_pairs : (nat64, vec text) -> (Result_6); - deploy_cdao_sns : (SnsInitPayload, nat64) -> (Result_7); + create_a_namespace : (text) -> (Result_5); + delete_key_value_pair : (nat64, text) -> (Result_6); + delete_multiple_key_value_pairs : (nat64, vec text) -> (Result_7); + deploy_cdao_sns : (SnsInitPayload, nat64) -> (Result_8); deployed_cdao_canisters : () -> (vec DeployedCdaoCanisters) query; - do_i_follow_this_user : (FolloweeArg) -> (Result_8) query; + distribute_newly_created_token_to_token_chain : (DeployedCdaoCanisters) -> ( + Result_3, + ); + do_i_follow_this_user : (FolloweeArg) -> (Result_9) query; download_snapshot : (nat64, nat64) -> (blob) query; - get_bet_details_for_a_user_on_a_post : (principal, nat64) -> (Result_9) query; + get_bet_details_for_a_user_on_a_post : (principal, nat64) -> ( + Result_10, + ) query; get_device_identities : () -> (vec DeviceIdentity) query; - get_entire_individual_post_detail_by_id : (nat64) -> (Result_10) query; + get_entire_individual_post_detail_by_id : (nat64) -> (Result_11) query; get_hot_or_not_bet_details_for_this_post : (nat64) -> (BettingStatus) query; get_hot_or_not_bets_placed_by_this_profile_with_pagination : (nat64) -> ( vec PlacedBetDetail, @@ -571,14 +591,14 @@ service : (IndividualUserTemplateInitArgs) -> { opt PlacedBetDetail, ) query; get_individual_post_details_by_id : (nat64) -> (PostDetailsForFrontend) query; - get_last_access_time : () -> (Result_11) query; - get_last_canister_functionality_access_time : () -> (Result_11) query; + get_last_access_time : () -> (Result_12) query; + get_last_canister_functionality_access_time : () -> (Result_12) query; get_ml_feed_cache_paginated : (nat64, nat64) -> (vec MLFeedCacheItem) query; get_posts_of_this_user_profile_with_pagination : (nat64, nat64) -> ( - Result_12, + Result_13, ) query; get_posts_of_this_user_profile_with_pagination_cursor : (nat64, nat64) -> ( - Result_12, + Result_13, ) query; get_principals_that_follow_this_profile_paginated : (opt nat64) -> ( vec record { nat64; FollowEntryDetail }, @@ -590,43 +610,45 @@ service : (IndividualUserTemplateInitArgs) -> { get_profile_details_v2 : () -> (UserProfileDetailsForFrontendV2) query; get_rewarded_for_referral : (principal, principal) -> (); get_rewarded_for_signing_up : () -> (); - get_session_type : () -> (Result_13) query; + get_session_type : () -> (Result_14) query; get_stable_memory_size : () -> (nat64) query; - get_success_history : () -> (Result_14) query; + get_success_history : () -> (Result_15) query; get_token_roots_of_this_user_with_pagination_cursor : (nat64, nat64) -> ( - Result_15, + Result_16, ) query; get_user_caniser_cycle_balance : () -> (nat) query; get_user_utility_token_transaction_history_with_pagination : ( nat64, nat64, - ) -> (Result_16) query; + ) -> (Result_17) query; get_utility_token_balance : () -> (nat64) query; get_version : () -> (text) query; get_version_number : () -> (nat64) query; - get_watch_history : () -> (Result_17) query; + get_watch_history : () -> (Result_18) query; get_well_known_principal_value : (KnownPrincipalType) -> ( opt principal, ) query; http_request : (HttpRequest) -> (HttpResponse) query; - list_namespace_keys : (nat64) -> (Result_18) query; + list_namespace_keys : (nat64) -> (Result_19) query; list_namespaces : (nat64, nat64) -> (vec NamespaceForFrontend) query; load_snapshot : (nat64) -> (); - once_reenqueue_timers_for_pending_bet_outcomes : () -> (Result_19); - read_key_value_pair : (nat64, text) -> (Result_5) query; + once_reenqueue_timers_for_pending_bet_outcomes : () -> (Result_20); + parent_airdrop_chain : () -> (vec AirdropMember) query; + read_key_value_pair : (nat64, text) -> (Result_6) query; receive_and_save_snaphot : (nat64, blob) -> (); - receive_bet_from_bet_makers_canister : (PlaceBetArg, principal) -> (Result_3); + receive_bet_from_bet_makers_canister : (PlaceBetArg, principal) -> (Result_4); receive_bet_winnings_when_distributed : (nat64, BetOutcomeForBetMaker) -> (); - receive_data_from_hotornot : (principal, nat64, vec Post) -> (Result_20); + receive_data_from_hotornot : (principal, nat64, vec Post) -> (Result_21); + receive_reward_for_being_referred : () -> (Result_3); + receive_reward_for_referring : (ProofOfParticipation, principal) -> ( + Result_3, + ); return_cycles_to_user_index_canister : (opt nat) -> (); save_snapshot_json : () -> (nat32); settle_neurons_fund_participation : ( SettleNeuronsFundParticipationRequest, ) -> (SettleNeuronsFundParticipationResponse); - transfer_token_to_user_canister : (principal, principal, opt blob, nat) -> ( - Result_23, - ); - transfer_tokens_and_posts : (principal, principal) -> (Result_20); + transfer_tokens_and_posts : (principal, principal) -> (Result_21); update_last_access_time : () -> (Result_24); update_last_canister_functionality_access_time : () -> (); update_ml_feed_cache : (vec MLFeedCacheItem) -> (Result_24); @@ -638,21 +660,21 @@ service : (IndividualUserTemplateInitArgs) -> { update_profile_display_details : (UserProfileUpdateDetailsFromFrontend) -> ( Result_25, ); - update_profile_owner : (opt principal) -> (Result_26); - update_profile_set_unique_username_once : (text) -> (Result_27); + update_profile_owner : (opt principal) -> (Result_3); + update_profile_set_unique_username_once : (text) -> (Result_26); update_profiles_i_follow_toggle_list_with_specified_profile : ( FolloweeArg, - ) -> (Result_8); + ) -> (Result_9); update_profiles_that_follow_me_toggle_list_with_specified_profile : ( FollowerArg, - ) -> (Result_8); + ) -> (Result_9); update_referrer_details : (UserCanisterDetails) -> (Result_24); update_session_type : (SessionType) -> (Result_24); update_success_history : (SuccessHistoryItemV1) -> (Result_24); update_watch_history : (WatchHistoryItem) -> (Result_24); update_well_known_principal : (KnownPrincipalType, principal) -> (); - write_key_value_pair : (nat64, text, text) -> (Result_5); + write_key_value_pair : (nat64, text, text) -> (Result_6); write_multiple_key_value_pairs : (nat64, vec record { text; text }) -> ( - Result_6, + Result_7, ); } diff --git a/src/canister/individual_user_template/src/api/airdrop/mod.rs b/src/canister/individual_user_template/src/api/airdrop/mod.rs new file mode 100644 index 00000000..291c72b0 --- /dev/null +++ b/src/canister/individual_user_template/src/api/airdrop/mod.rs @@ -0,0 +1,108 @@ +use candid::{Nat, Principal}; +use futures::{future, stream::FuturesUnordered, StreamExt}; +use ic_cdk::{update, query}; +use icrc_ledger_types::icrc1::{account::Account, transfer::{TransferArg, TransferError}}; +use shared_utils::{canister_specific::individual_user_template::types::{airdrop::AirdropMember, cdao::DeployedCdaoCanisters}, common::participant_crypto::ProofOfParticipation}; + +use crate::CANISTER_DATA; + +#[update] +pub async fn add_user_to_airdrop_chain(pop: ProofOfParticipation, member: AirdropMember) -> Result<(), String> { + pop.verify_caller_is_participant(&CANISTER_DATA).await?; + add_user_to_airdrop_chain_inner(member).await; + + Ok(()) +} + + +/// Returns the token amount to transfer for airdrop +/// returns None if not enough tokens are available +/// returns Ok(Some(Nat)) if enough tokens are available, where Nat is the amount to transfer +pub(crate) async fn is_balance_enough_for_airdrop(ledger: Principal, transfer_cnt: usize) -> Result, String> { + // SNS Tokens have 8 decimals + // 1e8 e8s -> 1 token + let base_amount = Nat::from(1e8 as usize); + + let fee: (Nat,) = ic_cdk::call(ledger, "icrc1_fee", ()).await.map_err(|(_, err)| err)?; + let fee = fee.0; + let transfer_amt = (base_amount.clone() + fee) * transfer_cnt; + + let acc = Account { owner: ic_cdk::id(), subaccount: None }; + let bal: (Nat,) = ic_cdk::call(ledger, "icrc1_balance_of", (acc,)).await.map_err(|(_, err)| err)?; + + if transfer_amt > bal.0 { + return Ok(None); + } + + Ok(Some(base_amount)) +} + +/// Transfer tokens for airdrop to target user_canister +/// returns Ok(Some(Principal)) if the transfer was successful +/// returns Ok(None) if transfer is not available +async fn transfer_token_for_airdrop(canisters: DeployedCdaoCanisters, member: AirdropMember) -> Result, String> { + let ledger = canisters.ledger; + let Some(amount) = is_balance_enough_for_airdrop(ledger, 1).await? else { + return Ok(None); + }; + + let transfer_args = TransferArg { + from_subaccount: None, + to: Account { owner: member.user_principal, subaccount: None }, + fee: None, + created_at_time: None, + memo: None, + amount, + }; + let transfer_res: (Result,) = ic_cdk::call(ledger, "icrc1_transfer", (transfer_args,)) + .await + .map_err(|(_, err)| err)?; + if transfer_res.0.is_err() { + return Err("transfer failed".into()); + } + + Ok(Some(canisters.root)) +} + +/// Add The user to the airdrop chain +/// also airdrops all the created tokens to this user +async fn add_user_to_airdrop_chain_inner(member: AirdropMember) { + let was_inserted = CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.airdrop.token_chain.insert(member) + }); + + if !was_inserted { + return; + } + + let my_tokens = CANISTER_DATA.with_borrow(|cdata| cdata.cdao_canisters.clone()); + airdrop_tokens_to_user(member, &my_tokens).await; +} + +/// Airdrop all created tokens to this user +pub(crate) async fn airdrop_tokens_to_user(member: AirdropMember, tokens: &[DeployedCdaoCanisters]) { + let transferred_tokens = tokens + .iter() + .map(|canisters| transfer_token_for_airdrop(*canisters, member)) + .collect::>() + .filter_map(|res| { + let Ok(Some(res)) = res else { + return future::ready(None); + }; + future::ready(Some(res)) + }) + .collect::>() + .await; + + // Rollback if notify fails + ic_cdk::notify( + member.user_canister, + "add_tokens", + (transferred_tokens,) + ).unwrap(); +} + +#[query] +pub fn parent_airdrop_chain() -> Vec { + CANISTER_DATA.with_borrow(|cdata| cdata.airdrop.parent_chain.clone()) +} \ No newline at end of file diff --git a/src/canister/individual_user_template/src/api/canister_lifecycle/init.rs b/src/canister/individual_user_template/src/api/canister_lifecycle/init.rs index 3bb00f1d..45eac9d9 100644 --- a/src/canister/individual_user_template/src/api/canister_lifecycle/init.rs +++ b/src/canister/individual_user_template/src/api/canister_lifecycle/init.rs @@ -31,6 +31,9 @@ fn init_impl(init_args: IndividualUserTemplateInitArgs, data: &mut CanisterData) data.version_details.version_number = init_args.upgrade_version_number.unwrap_or_default(); data.version_details.version = init_args.version; + if let Some(pop) = init_args.proof_of_participation { + data.proof_of_participation = Some(pop); + } } pub fn send_canister_metrics() { @@ -78,7 +81,8 @@ mod test { url_to_send_canister_metrics_to: Some( "http://metrics-url.com/receive-metrics".to_string(), ), - version: String::from("v1.0.0") + version: String::from("v1.0.0"), + proof_of_participation: None, }; let mut data = CanisterData::default(); diff --git a/src/canister/individual_user_template/src/api/canister_lifecycle/post_upgrade.rs b/src/canister/individual_user_template/src/api/canister_lifecycle/post_upgrade.rs index f0943231..7197f3bf 100644 --- a/src/canister/individual_user_template/src/api/canister_lifecycle/post_upgrade.rs +++ b/src/canister/individual_user_template/src/api/canister_lifecycle/post_upgrade.rs @@ -66,6 +66,10 @@ fn save_upgrade_args_to_memory() { .configuration .url_to_send_canister_metrics_to = Some(url_to_send_canister_metrics_to); } + + if let Some(pop) = upgrade_args.proof_of_participation { + canister_data_ref_cell.proof_of_participation = Some(pop); + } }); } @@ -76,4 +80,4 @@ fn migrate_excessive_tokens(){ canister_data_ref_cell.my_token_balance.utility_token_balance = 1000; } }); -} \ No newline at end of file +} diff --git a/src/canister/individual_user_template/src/api/cdao/mod.rs b/src/canister/individual_user_template/src/api/cdao/mod.rs index 04fb0a2d..2d250b43 100644 --- a/src/canister/individual_user_template/src/api/cdao/mod.rs +++ b/src/canister/individual_user_template/src/api/cdao/mod.rs @@ -1,19 +1,19 @@ -mod token; +pub(crate) mod token; use std::collections::VecDeque; use candid::{Encode, Principal}; use futures::{ stream::{FuturesOrdered, FuturesUnordered}, - StreamExt, TryStreamExt, + TryStreamExt, }; use ic_base_types::PrincipalId; use ic_cdk::{ api::{ call::RejectionCode, management_canister::main::{ - create_canister, deposit_cycles, install_code, update_settings, CanisterIdRecord, - CanisterInstallMode, CanisterSettings, CreateCanisterArgument, InstallCodeArgument, + deposit_cycles, install_code, update_settings, CanisterIdRecord, + CanisterInstallMode, CanisterSettings, InstallCodeArgument, UpdateSettingsArgument, }, }, @@ -34,6 +34,7 @@ use shared_utils::{ common::types::known_principal::KnownPrincipalType, constant::{NNS_LEDGER_CANISTER_ID, USER_SNS_CANISTER_INITIAL_CYCLES}, }; +use token::transfer_canister_token_to_user_principal; use crate::{ util::{ @@ -42,6 +43,8 @@ use crate::{ CANISTER_DATA, }; +use super::airdrop::is_balance_enough_for_airdrop; + #[update] pub async fn settle_neurons_fund_participation( request: SettleNeuronsFundParticipationRequest, @@ -283,3 +286,41 @@ async fn deploy_cdao_sns( Ok(deployed_cans) } + +/// Destributes the newly created to all users in the existing airdrop token chain +/// this must be called right after tokens have been swapped to this user canister +/// by the off-chain infrastructure +#[update] +async fn distribute_newly_created_token_to_token_chain(token: DeployedCdaoCanisters) -> Result<(), String> { + let caller = ic_cdk::caller(); + let profile_owner = CANISTER_DATA.with_borrow(|cdata| cdata.profile.principal_id); + if Some(caller) != profile_owner { + return Err("unauthorized".into()); + } + + let token_root = token.root; + let token_ledger = token.ledger; + let token_chain = CANISTER_DATA.with_borrow(|cdata| { + cdata.airdrop.token_chain.clone() + }); + let amount = is_balance_enough_for_airdrop(token_ledger, token_chain.len()) + .await? + .ok_or_else(move || "not token enough balance".to_string())?; + + for member in token_chain { + let amount = amount.clone(); + ic_cdk::spawn(async move { + // ignore the result in case this fails + _ = transfer_canister_token_to_user_principal( + token_root, + token_ledger, + member.user_principal, + member.user_canister, + None, + amount, + ).await; + }); + } + + Ok(()) +} diff --git a/src/canister/individual_user_template/src/api/cdao/token.rs b/src/canister/individual_user_template/src/api/cdao/token.rs index b51cd6f0..5db64314 100644 --- a/src/canister/individual_user_template/src/api/cdao/token.rs +++ b/src/canister/individual_user_template/src/api/cdao/token.rs @@ -1,4 +1,5 @@ use candid::{Nat, Principal}; +use futures::{stream::FuturesUnordered, StreamExt}; use ic_cdk::{query, update}; use ic_sns_root::pb::v1::{ListSnsCanistersRequest, ListSnsCanistersResponse}; use icrc_ledger_types::icrc1::{account::Account, transfer::{Memo, TransferArg, TransferError}}; @@ -18,6 +19,20 @@ async fn add_token(root_canister: Principal) -> Result { return Ok(false); } + if is_token_balance_zero(root_canister).await? { + return Err(CdaoTokenError::NoBalance); + } + + CANISTER_DATA.with(|cdata| { + let mut cdata = cdata.borrow_mut(); + cdata.token_roots.insert(root_canister, ()); + }); + + + Ok(true) +} + +async fn is_token_balance_zero(root_canister: Principal) -> Result { let res: (ListSnsCanistersResponse,) = ic_cdk::call(root_canister, "list_sns_canisters", (ListSnsCanistersRequest {},)).await?; let cans = res.0; let ledger = cans.ledger.ok_or(CdaoTokenError::InvalidRoot)?; @@ -26,42 +41,61 @@ async fn add_token(root_canister: Principal) -> Result { .with(|canister_data_ref_cell| canister_data_ref_cell.borrow().profile.principal_id) .ok_or(CdaoTokenError::Unauthenticated)?; let acc = Account { owner: my_principal_id, subaccount: None }; + let balance: (Nat,) = ic_cdk::call(ledger.into(), "icrc1_balance_of", (acc,)).await?; - if balance.0 == 0u32 { - return Err(CdaoTokenError::NoBalance); - } + Ok(balance.0 == 0u32) +} - CANISTER_DATA.with(|cdata| { - let mut cdata = cdata.borrow_mut(); - cdata.token_roots.insert(root_canister, ()); +/// Add multiple tokens +/// returns a list specifying which tokens were added (or failed to be added) +/// the list matches the order of the input list +#[update] +async fn add_tokens(root_canisters: Vec) -> Vec> { + let mut result = vec![]; + let filtered_roots: Vec<_> = CANISTER_DATA.with_borrow(|cdata| { + root_canisters.into_iter().enumerate().filter(|(_, root)| { + let already_added = cdata.token_roots.contains_key(root); + result.push(Ok(!already_added)); + !already_added + }).collect() }); + let mut is_balances_zero = filtered_roots + .into_iter() + .map(|(i, root)| async move { + (i, root, is_token_balance_zero(root).await) + }) + .collect::>(); - return Ok(true); -} + while let Some((i, root, is_balance_zero_res)) = is_balances_zero.next().await { + let is_balance_zero = is_balance_zero_res.as_ref().copied().unwrap_or_default(); + result[i] = is_balance_zero_res; -#[update] -async fn transfer_token_to_user_canister(token_root: Principal, target_canister: Principal, memo: Option, amount: Nat) -> Result<(), CdaoTokenError> { - // * access control - let current_caller = ic_cdk::caller(); - let my_principal_id = CANISTER_DATA - .with(|canister_data_ref_cell| canister_data_ref_cell.borrow().profile.principal_id); - if my_principal_id != Some(current_caller) { - return Err(CdaoTokenError::Unauthenticated); - }; + if is_balance_zero { + continue; + } - let res: (ListSnsCanistersResponse,) = ic_cdk::call(token_root, "list_sns_canisters", (ListSnsCanistersRequest {},)).await?; - let ledger = res.0.ledger.ok_or(CdaoTokenError::InvalidRoot)?; + CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.token_roots.insert(root, ()); + }); + } + + result +} +/// Transfer some tokens from this canister's balance to another user's principal +/// NOTE: target_canister must be the canister of the user. This check is not performed in this function. +/// token_ledger must be the ledger of the token root, this function does not perform this check. +pub(crate) async fn transfer_canister_token_to_user_principal(token_root: Principal, token_ledger: Principal, target_user_principal: Principal, target_canister: Principal, memo: Option, amount: Nat) -> Result<(), CdaoTokenError> { let transfer_args = TransferArg { from_subaccount: None, - to: Account { owner: target_canister, subaccount: None }, + to: Account { owner: target_user_principal, subaccount: None }, fee: None, created_at_time: None, memo, amount, }; - let transfer_res: (Result,) = ic_cdk::call(ledger.into(), "icrc1_transfer", (transfer_args,)).await?; + let transfer_res: (Result,) = ic_cdk::call(token_ledger, "icrc1_transfer", (transfer_args,)).await?; transfer_res.0.map_err(CdaoTokenError::Transfer)?; let res: (Result,) = ic_cdk::call(target_canister, "add_token", (token_root,)).await?; diff --git a/src/canister/individual_user_template/src/api/mod.rs b/src/canister/individual_user_template/src/api/mod.rs index b750601a..04dd7561 100644 --- a/src/canister/individual_user_template/src/api/mod.rs +++ b/src/canister/individual_user_template/src/api/mod.rs @@ -14,3 +14,5 @@ pub mod token; pub mod well_known_principal; pub mod cdao; pub mod device_id_management; +pub mod referral; +pub mod airdrop; diff --git a/src/canister/individual_user_template/src/api/referral/mod.rs b/src/canister/individual_user_template/src/api/referral/mod.rs new file mode 100644 index 00000000..de8b545c --- /dev/null +++ b/src/canister/individual_user_template/src/api/referral/mod.rs @@ -0,0 +1,136 @@ +use candid::Principal; +use futures::{stream::FuturesUnordered, StreamExt}; +use ic_cdk::{call, notify, update}; +use shared_utils::{canister_specific::individual_user_template::types::{airdrop::AirdropMember, session::SessionType}, common::{participant_crypto::ProofOfParticipation, types::utility_token::token_event::{MintEvent, TokenEvent}, utils::system_time}}; + +use crate::{api::canister_management::update_last_access_time::update_last_canister_functionality_access_time, CANISTER_DATA}; + +use super::airdrop::airdrop_tokens_to_user; + +pub(crate) fn coyn_token_reward_for_referral(referrer: Principal, referree: Principal) { + let current_time = system_time::get_current_system_time_from_ic(); + + CANISTER_DATA.with_borrow_mut(|cdata| { + let my_token_balance = &mut cdata.my_token_balance; + + let referral_reward_amount = TokenEvent::get_token_amount_for_token_event(&TokenEvent::Mint { + amount: 0, + details: MintEvent::Referral { + referee_user_principal_id: referree, + referrer_user_principal_id: referrer, + }, + timestamp: current_time, + }); + + my_token_balance.handle_token_event(TokenEvent::Mint { + amount: referral_reward_amount, + details: MintEvent::Referral { + referrer_user_principal_id: referrer, + referee_user_principal_id: referree, + }, + timestamp: current_time, + }); + }) +} + +#[update] +pub async fn receive_reward_for_being_referred() -> Result<(), String> { + let (pop, user_principal, referrer_details, session_type, has_parent) = CANISTER_DATA.with_borrow(|cdata| { + let profile = &cdata.profile; + ( + cdata.proof_of_participation.clone(), + profile.principal_id, + profile.referrer_details.clone(), + cdata.session_type, + !cdata.airdrop.parent_chain.is_empty() + ) + }); + + let Some(pop) = pop else { + return Err("method is not available right now".into()); + }; + + let Some(user_principal) = user_principal else { + return Err("canister is not ready".into()); + }; + + let Some(referrer_details) = referrer_details else { + return Err("no referrer details found".into()); + }; + + if session_type != Some(SessionType::RegisteredSession) { + return Err("user not signed up".into()); + } + + if has_parent { + return Err("User has already claimed the reward".into()); + } + + update_last_canister_functionality_access_time(); + + coyn_token_reward_for_referral(referrer_details.profile_owner, user_principal); + + let (mut parents,): (Vec,) = call( + referrer_details.user_canister_id, + "parent_airdrop_chain", + () + ).await.expect("Invalid parent"); + + let referrer_member = AirdropMember { + user_canister: referrer_details.user_canister_id, + user_principal: referrer_details.profile_owner, + }; + parents.push(referrer_member); + + let my_tokens = CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.airdrop.parent_chain = parents.clone(); + cdata.airdrop.token_chain.extend(&parents); + + cdata.cdao_canisters.clone() + }); + + let parents_c = parents.clone(); + ic_cdk::spawn(async move { + let mut transfers = parents_c + .into_iter() + .map(|member| airdrop_tokens_to_user(member, &my_tokens)) + .collect::>(); + + while transfers.next().await.is_some() {} + }); + + let me_airdrop = AirdropMember { + user_principal, + user_canister: ic_cdk::id(), + }; + for parent in parents.iter() { + notify( + parent.user_canister, + "add_user_to_airdrop_chain", + (pop.clone(), me_airdrop) + ).unwrap() + } + + // Rollback if the notification fails + notify( + referrer_details.user_canister_id, + "receive_reward_for_referring", + (pop, user_principal) + ).map_err(|_| "failed to reward referrer".to_string()) + .unwrap(); + + Ok(()) +} + +#[update] +pub async fn receive_reward_for_referring(pop: ProofOfParticipation, referree_principal: Principal) -> Result<(), String> { + pop.verify_caller_is_participant(&CANISTER_DATA).await?; + + let Some(profile_owner) = CANISTER_DATA.with_borrow(|cdata| cdata.profile.principal_id) else { + return Err("canister is not ready".into()); + }; + + coyn_token_reward_for_referral(profile_owner, referree_principal); + + Ok(()) +} diff --git a/src/canister/individual_user_template/src/api/token/get_rewarded_for_referral.rs b/src/canister/individual_user_template/src/api/token/get_rewarded_for_referral.rs index af432e4f..e0d620f4 100644 --- a/src/canister/individual_user_template/src/api/token/get_rewarded_for_referral.rs +++ b/src/canister/individual_user_template/src/api/token/get_rewarded_for_referral.rs @@ -1,17 +1,12 @@ use crate::{ - api::canister_management::update_last_access_time::update_last_canister_functionality_access_time, + api::{canister_management::update_last_access_time::update_last_canister_functionality_access_time, referral::coyn_token_reward_for_referral}, CANISTER_DATA, }; use candid::Principal; use ic_cdk_macros::update; -use shared_utils::common::{ - types::{ - known_principal::KnownPrincipalType, - utility_token::token_event::{MintEvent, TokenEvent}, - }, - utils::system_time, -}; +use shared_utils::common::types::known_principal::KnownPrincipalType; +#[deprecated = "use new methods in crate::api::referral"] #[update] fn get_rewarded_for_referral(referrer: Principal, referree: Principal) { // * access control @@ -31,28 +26,5 @@ fn get_rewarded_for_referral(referrer: Principal, referree: Principal) { update_last_canister_functionality_access_time(); - let current_time = system_time::get_current_system_time_from_ic(); - - CANISTER_DATA.with(|canister_data_ref_cell| { - let my_token_balance = &mut canister_data_ref_cell.borrow_mut().my_token_balance; - - let referral_reward_amount = - TokenEvent::get_token_amount_for_token_event(&TokenEvent::Mint { - amount: 0, - details: MintEvent::Referral { - referrer_user_principal_id: referrer, - referee_user_principal_id: referree, - }, - timestamp: current_time, - }); - - my_token_balance.handle_token_event(TokenEvent::Mint { - amount: referral_reward_amount, - details: MintEvent::Referral { - referrer_user_principal_id: referrer, - referee_user_principal_id: referree, - }, - timestamp: current_time, - }); - }); + coyn_token_reward_for_referral(referrer, referree); } diff --git a/src/canister/individual_user_template/src/data_model/airdrop.rs b/src/canister/individual_user_template/src/data_model/airdrop.rs new file mode 100644 index 00000000..2229d394 --- /dev/null +++ b/src/canister/individual_user_template/src/data_model/airdrop.rs @@ -0,0 +1,13 @@ +use std::collections::HashSet; + +use serde::{Serialize, Deserialize}; +use shared_utils::canister_specific::individual_user_template::types::airdrop::AirdropMember; + + +#[derive(Default, Deserialize, Serialize)] +pub struct AirdropData { + #[serde(default)] + pub token_chain: HashSet, + #[serde(default)] + pub parent_chain: Vec, +} diff --git a/src/canister/individual_user_template/src/data_model/mod.rs b/src/canister/individual_user_template/src/data_model/mod.rs index 95962131..20a34a3e 100644 --- a/src/canister/individual_user_template/src/data_model/mod.rs +++ b/src/canister/individual_user_template/src/data_model/mod.rs @@ -3,6 +3,7 @@ use std::{ time::SystemTime, }; +use airdrop::AirdropData; use candid::{Deserialize, Principal}; use ic_cdk::api::management_canister::provisional::CanisterId; use memory::{get_success_history_memory, get_token_list_memory, get_watch_history_memory}; @@ -24,12 +25,12 @@ use shared_utils::{ session::SessionType, token::TokenBalance, }, - common::types::{ + common::{participant_crypto::{ProofOfParticipationStore, ProofOfParticipation, PubKeyCache}, types::{ app_primitive_type::PostId, - known_principal::KnownPrincipalMap, + known_principal::{KnownPrincipalMap, KnownPrincipalType}, top_posts::{post_score_index::PostScoreIndex, post_score_index_item::PostStatus}, version_details::VersionDetails, - }, + }}, }; use self::memory::{ @@ -41,6 +42,7 @@ use kv_storage::AppStorage; pub mod kv_storage; pub mod memory; +pub mod airdrop; #[derive(Deserialize, Serialize)] pub struct CanisterData { @@ -91,6 +93,12 @@ pub struct CanisterData { // list of root token canisters #[serde(skip, default = "_default_token_list")] pub token_roots: ic_stable_structures::btreemap::BTreeMap, + #[serde(default)] + pub proof_of_participation: Option, + #[serde(default)] + pub pubkey_cache: PubKeyCache, + #[serde(default)] + pub airdrop: AirdropData, } pub fn _default_room_details( @@ -163,6 +171,23 @@ impl Default for CanisterData { ml_feed_cache: Vec::new(), cdao_canisters: Vec::new(), token_roots: _default_token_list(), + proof_of_participation: None, + pubkey_cache: PubKeyCache::default(), + airdrop: AirdropData::default(), } } } + +impl ProofOfParticipationStore for CanisterData { + fn pubkey_cache(&self) -> &PubKeyCache { + &self.pubkey_cache + } + + fn pubkey_cache_mut(&mut self) -> &mut PubKeyCache { + &mut self.pubkey_cache + } + + fn platform_orchestrator(&self) -> Principal { + self.known_principal_ids[&KnownPrincipalType::CanisterIdPlatformOrchestrator] + } +} diff --git a/src/canister/individual_user_template/src/lib.rs b/src/canister/individual_user_template/src/lib.rs index a318e4e5..9d2935e1 100644 --- a/src/canister/individual_user_template/src/lib.rs +++ b/src/canister/individual_user_template/src/lib.rs @@ -5,7 +5,7 @@ use api::{ follow::update_profiles_that_follow_me_toggle_list_with_specified_profile::FollowerArg, profile::update_profile_display_details::UpdateProfileDetailsError, }; -use candid::{Nat, Principal}; +use candid::Principal; use data_model::CanisterData; use ic_cdk::api::management_canister::provisional::CanisterId; use ic_cdk_macros::export_candid; @@ -13,9 +13,9 @@ use ic_nns_governance::pb::v1::{ SettleNeuronsFundParticipationRequest, SettleNeuronsFundParticipationResponse, }; use ic_sns_init::pb::v1::SnsInitPayload; -use icrc_ledger_types::icrc1::transfer::Memo; use shared_utils::{ canister_specific::individual_user_template::types::{ + airdrop::AirdropMember, arg::{FolloweeArg, IndividualUserTemplateInitArgs, PlaceBetArg}, cdao::DeployedCdaoCanisters, device_id::DeviceIdentity, @@ -32,17 +32,20 @@ use shared_utils::{ Post, PostDetailsForFrontend, PostDetailsFromFrontend, PostViewDetailsFromFrontend, }, profile::{ - UserCanisterDetails, UserProfile, UserProfileDetailsForFrontend, + UserCanisterDetails, UserProfileDetailsForFrontend, UserProfileDetailsForFrontendV2, UserProfileUpdateDetailsFromFrontend, }, session::SessionType, }, - common::types::{ - app_primitive_type::PostId, - http::{HttpRequest, HttpResponse}, - known_principal::KnownPrincipalType, - top_posts::post_score_index_item::PostStatus, - utility_token::token_event::TokenEvent, + common::{ + participant_crypto::ProofOfParticipation, + types::{ + app_primitive_type::PostId, + http::{HttpRequest, HttpResponse}, + known_principal::KnownPrincipalType, + top_posts::post_score_index_item::PostStatus, + utility_token::token_event::TokenEvent, + } }, pagination::PaginationError, types::canister_specific::individual_user_template::error_types::{ diff --git a/src/canister/platform_orchestrator/Cargo.toml b/src/canister/platform_orchestrator/Cargo.toml index f36f68ee..eccfa50d 100644 --- a/src/canister/platform_orchestrator/Cargo.toml +++ b/src/canister/platform_orchestrator/Cargo.toml @@ -20,3 +20,4 @@ shared_utils = { workspace = true } [dev-dependencies] test_utils = { workspace = true } + diff --git a/src/canister/platform_orchestrator/src/api/canister_lifecycle/post_upgrade.rs b/src/canister/platform_orchestrator/src/api/canister_lifecycle/post_upgrade.rs index 804c1302..bff21a44 100644 --- a/src/canister/platform_orchestrator/src/api/canister_lifecycle/post_upgrade.rs +++ b/src/canister/platform_orchestrator/src/api/canister_lifecycle/post_upgrade.rs @@ -3,7 +3,7 @@ use ic_cdk::api::call::ArgDecoderConfig; use ic_cdk_macros::post_upgrade; use ic_stable_structures::Memory; use shared_utils::{ - canister_specific::platform_orchestrator::types::args::PlatformOrchestratorInitArgs, + canister_specific::{platform_orchestrator::types::args::PlatformOrchestratorInitArgs, post_cache}, common::utils::system_time, }; @@ -13,6 +13,7 @@ use crate::{data_model::memory, CANISTER_DATA}; pub fn post_upgrade() { restore_data_from_stable_memory(); update_version_from_args(); + initialize_children_merkle(); } fn restore_data_from_stable_memory() { @@ -38,3 +39,13 @@ fn update_version_from_args() { canister_data.version_detail.last_update_on = system_time::get_current_system_time(); }) } + +// TODO: remove this once the upgrade is complete +fn initialize_children_merkle() { + CANISTER_DATA.with_borrow_mut(|cdata| { + let mut children: Vec<_> = cdata.subnet_orchestrators().iter().copied().collect(); + children.extend(cdata.post_cache_orchestrators()); + + cdata.children_merkle.insert_children(children) + }); +} diff --git a/src/canister/platform_orchestrator/src/api/canister_management/deregister_subnet_orchestrator.rs b/src/canister/platform_orchestrator/src/api/canister_management/deregister_subnet_orchestrator.rs index a9b7343a..c10b2f87 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/deregister_subnet_orchestrator.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/deregister_subnet_orchestrator.rs @@ -6,14 +6,6 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] fn deregister_subnet_orchestrator(canister_id: Principal, remove_it_completely: bool) { CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data - .subet_orchestrator_with_capacity_left - .remove(&canister_id); - - if remove_it_completely { - canister_data - .all_subnet_orchestrator_canisters_list - .remove(&canister_id); - } + canister_data.remove_subnet_orchestrator(canister_id, remove_it_completely); }); } diff --git a/src/canister/platform_orchestrator/src/api/canister_management/get_all_subnet_orchestrators.rs b/src/canister/platform_orchestrator/src/api/canister_management/get_all_subnet_orchestrators.rs index 0b1a61fd..d289022d 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/get_all_subnet_orchestrators.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/get_all_subnet_orchestrators.rs @@ -8,7 +8,7 @@ use crate::CANISTER_DATA; #[query] fn get_all_subnet_orchestrators() -> Vec { CANISTER_DATA.with_borrow(|canister_data| { - let canisters = canister_data.all_subnet_orchestrator_canisters_list.iter().map(|canister_id| {*canister_id}).collect::>(); + let canisters = canister_data.subnet_orchestrators().iter().map(|canister_id| {*canister_id}).collect::>(); canisters }) } \ No newline at end of file diff --git a/src/canister/platform_orchestrator/src/api/canister_management/known_principal.rs b/src/canister/platform_orchestrator/src/api/canister_management/known_principal.rs index f055dd95..f5d7a0f5 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/known_principal.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/known_principal.rs @@ -75,7 +75,7 @@ async fn issue_update_known_principal_for_all_subnet( ) { let subnet_list: Vec = CANISTER_DATA.with_borrow(|canister_data| { canister_data - .all_subnet_orchestrator_canisters_list + .subnet_orchestrators() .iter() .copied() .collect() diff --git a/src/canister/platform_orchestrator/src/api/canister_management/populate_known_principal_for_all_subnet.rs b/src/canister/platform_orchestrator/src/api/canister_management/populate_known_principal_for_all_subnet.rs index 4646a511..b1c41f59 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/populate_known_principal_for_all_subnet.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/populate_known_principal_for_all_subnet.rs @@ -10,7 +10,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] async fn populate_known_principal_for_all_subnet() { let subnet_orchestrators: Vec = CANISTER_DATA.with_borrow(|canister_data| { - canister_data.all_subnet_orchestrator_canisters_list.iter().copied().collect() + canister_data.subnet_orchestrators().iter().copied().collect() }); for subnet_id in subnet_orchestrators { diff --git a/src/canister/platform_orchestrator/src/api/canister_management/provision_subnet_orchestrator.rs b/src/canister/platform_orchestrator/src/api/canister_management/provision_subnet_orchestrator.rs index fea506b2..18d2b974 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/provision_subnet_orchestrator.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/provision_subnet_orchestrator.rs @@ -15,10 +15,10 @@ use shared_utils::{ canister_specific::{ post_cache::types::arg::PostCacheInitArgs, user_index::types::args::UserIndexInitArgs, }, - common::types::{ + common::{participant_crypto::ProofOfParticipation, types::{ known_principal::{KnownPrincipalMap, KnownPrincipalType}, wasm::WasmType, - }, + }}, constant::{ GLOBAL_SUPER_ADMIN_USER_ID, INDIVIDUAL_USER_CANISTER_RECHARGE_AMOUNT, NNS_CYCLE_MINTING_CANISTER, POST_CACHE_CANISTER_CYCLES_RECHARGE_AMOUMT, @@ -106,22 +106,20 @@ pub async fn provision_subnet_orchestrator_canister( ); CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data - .all_post_cache_orchestrator_list - .insert(post_cache_canister_id); - canister_data - .all_subnet_orchestrator_canisters_list - .insert(subnet_orchestrator_canister_id); - canister_data - .subet_orchestrator_with_capacity_left - .insert(subnet_orchestrator_canister_id); + canister_data.insert_subnet_orchestrator_and_post_cache( + subnet_orchestrator_canister_id, + post_cache_canister_id + ); }); + let mut proof_of_participation = ProofOfParticipation::new_for_root(); + proof_of_participation = proof_of_participation.derive_for_child(&CANISTER_DATA, subnet_orchestrator_canister_id).await.unwrap(); let user_index_init_arg = UserIndexInitArgs { known_principal_ids: Some(known_principal_map.clone()), access_control_map: None, version: CANISTER_DATA .with_borrow(|canister_data| canister_data.version_detail.version.clone()), + proof_of_participation: Some(proof_of_participation), }; let subnet_orchestrator_install_code_arg = InstallCodeArgument { diff --git a/src/canister/platform_orchestrator/src/api/canister_management/register_new_subnet_orhestrator.rs b/src/canister/platform_orchestrator/src/api/canister_management/register_new_subnet_orhestrator.rs index 3bad50e9..37e5708c 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/register_new_subnet_orhestrator.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/register_new_subnet_orhestrator.rs @@ -33,7 +33,7 @@ async fn register_new_subnet_orchestrator( if let Some(first_subnet_orchestrator) = CANISTER_DATA.with_borrow(|canister_data| { canister_data - .all_subnet_orchestrator_canisters_list + .subnet_orchestrators() .iter() .next() .copied() @@ -59,15 +59,10 @@ async fn register_new_subnet_orchestrator( } CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data - .all_subnet_orchestrator_canisters_list - .insert(new_subnet_orchestrator_caniter_id); - - if subnet_is_available_for_provisioning_individual_canister { - canister_data - .subet_orchestrator_with_capacity_left - .insert(new_subnet_orchestrator_caniter_id); - } + canister_data.insert_subnet_orchestrator( + new_subnet_orchestrator_caniter_id, + subnet_is_available_for_provisioning_individual_canister, + ); Ok(()) }) } diff --git a/src/canister/platform_orchestrator/src/api/canister_management/stop_upgrades_for_individual_user_canisters.rs b/src/canister/platform_orchestrator/src/api/canister_management/stop_upgrades_for_individual_user_canisters.rs index 9aed5630..bb2c7643 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/stop_upgrades_for_individual_user_canisters.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/stop_upgrades_for_individual_user_canisters.rs @@ -7,7 +7,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT async fn stop_upgrades_for_individual_user_canisters() -> Result { let subnet_orchestrator_list = CANISTER_DATA.with_borrow(|canister_data| { - canister_data.all_subnet_orchestrator_canisters_list.clone() + canister_data.subnet_orchestrators().clone() }); for subnet_orchestrator in subnet_orchestrator_list { diff --git a/src/canister/platform_orchestrator/src/api/canister_management/update_canisters_last_access_time.rs b/src/canister/platform_orchestrator/src/api/canister_management/update_canisters_last_access_time.rs index 6e9ed630..22c4ec9a 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/update_canisters_last_access_time.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/update_canisters_last_access_time.rs @@ -6,7 +6,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] async fn update_canisters_last_functionality_access_time() -> Result { let subnet_orchestrator_list = CANISTER_DATA - .with_borrow(|canister_data| canister_data.all_subnet_orchestrator_canisters_list.clone()); + .with_borrow(|canister_data| canister_data.subnet_orchestrators().clone()); for subnet_orchestrator in subnet_orchestrator_list { let result: CallResult<()> = call( diff --git a/src/canister/platform_orchestrator/src/api/canister_management/update_profile_owner_for_individual_users.rs b/src/canister/platform_orchestrator/src/api/canister_management/update_profile_owner_for_individual_users.rs index b931d98c..7c7d9856 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/update_profile_owner_for_individual_users.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/update_profile_owner_for_individual_users.rs @@ -5,7 +5,7 @@ use crate::CANISTER_DATA; #[update] fn update_profile_owner_for_individual_canisters() { CANISTER_DATA.with_borrow(|canister_data| { - canister_data.all_subnet_orchestrator_canisters_list.iter().for_each(|subnet_canster_id| { + canister_data.subnet_orchestrators().iter().for_each(|subnet_canster_id| { let _ = ic_cdk::notify(*subnet_canster_id, "update_profile_owner_for_individual_canisters", ()); }) }) diff --git a/src/canister/platform_orchestrator/src/api/canister_management/update_timers_for_hon_game.rs b/src/canister/platform_orchestrator/src/api/canister_management/update_timers_for_hon_game.rs index 3a5127b9..c8b8a1fe 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/update_timers_for_hon_game.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/update_timers_for_hon_game.rs @@ -6,7 +6,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] async fn update_restart_timers_hon_game() -> Result { let subnet_orchestrator_list = CANISTER_DATA - .with_borrow(|canister_data| canister_data.all_subnet_orchestrator_canisters_list.clone()); + .with_borrow(|canister_data| canister_data.subnet_orchestrators().clone()); for subnet_orchestrator in subnet_orchestrator_list { let result: CallResult<()> = call( diff --git a/src/canister/platform_orchestrator/src/api/canister_management/upgrade_canisters_in_network.rs b/src/canister/platform_orchestrator/src/api/canister_management/upgrade_canisters_in_network.rs index f1661b56..3f4664de 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/upgrade_canisters_in_network.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/upgrade_canisters_in_network.rs @@ -16,8 +16,7 @@ use shared_utils::{ post_cache::types::arg::PostCacheInitArgs, user_index::types::args::UserIndexInitArgs, }, common::{ - types::wasm::{CanisterWasm, WasmType}, - utils::{task::run_task_concurrently, upgrade_canister::upgrade_canister_util}, + participant_crypto::ProofOfParticipation, types::wasm::{CanisterWasm, WasmType}, utils::{task::run_task_concurrently, upgrade_canister::upgrade_canister_util} }, constant::{ POST_CACHE_CANISTER_CYCLES_RECHARGE_AMOUMT, POST_CACHE_CANISTER_CYCLES_THRESHOLD, @@ -63,7 +62,7 @@ async fn upgrade_individual_canisters(upgrade_arg: UpgradeCanisterArg) { canister_data.last_subnet_canister_upgrade_status.count = 0; }); let subnet_orchestrator_canisters = CANISTER_DATA - .with_borrow(|canister_data| canister_data.all_subnet_orchestrator_canisters_list.clone()); + .with_borrow(|canister_data| canister_data.subnet_orchestrators().clone()); for subnet_orchestrator in subnet_orchestrator_canisters.iter() { match recharge_subnet_orchestrator_if_needed(*subnet_orchestrator).await { @@ -119,9 +118,9 @@ async fn upgrade_subnet_canisters(upgrade_arg: UpgradeCanisterArg) { let canister_list = CANISTER_DATA.with_borrow(|canister_data| { match upgrade_arg.canister { - WasmType::PostCacheWasm => Ok(canister_data.all_post_cache_orchestrator_list.clone()), + WasmType::PostCacheWasm => Ok(canister_data.post_cache_orchestrators().clone()), WasmType::SubnetOrchestratorWasm => { - Ok(canister_data.all_subnet_orchestrator_canisters_list.clone()) + Ok(canister_data.subnet_orchestrators().clone()) } _ => Err(()), } @@ -255,6 +254,10 @@ async fn upgrade_subnet_orchestrator_canister( wasm: Vec, version: String, ) -> Result<(), String> { + // TODO: remove this when all subnet orchestrators are upgraded + let mut proof_of_participation = ProofOfParticipation::new_for_root(); + proof_of_participation = proof_of_participation.derive_for_child(&CANISTER_DATA, canister_id).await?; + let install_code_arg = InstallCodeArgument { mode: CanisterInstallMode::Upgrade(None), canister_id, @@ -263,6 +266,7 @@ async fn upgrade_subnet_orchestrator_canister( known_principal_ids: None, access_control_map: None, version, + proof_of_participation: Some(proof_of_participation), }) .unwrap(), }; diff --git a/src/canister/platform_orchestrator/src/api/canister_management/upgrade_specific_individual_canister.rs b/src/canister/platform_orchestrator/src/api/canister_management/upgrade_specific_individual_canister.rs index 668f0857..89588546 100644 --- a/src/canister/platform_orchestrator/src/api/canister_management/upgrade_specific_individual_canister.rs +++ b/src/canister/platform_orchestrator/src/api/canister_management/upgrade_specific_individual_canister.rs @@ -6,7 +6,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] fn upgrade_specific_individual_canister(individual_canister_id: Principal) { CANISTER_DATA.with_borrow(|canister_data| { - canister_data.all_subnet_orchestrator_canisters_list.iter().for_each(|subnet_id| { + canister_data.subnet_orchestrators().iter().for_each(|subnet_id| { let _ = ic_cdk::notify(*subnet_id, "upgrade_specific_individual_user_canister_with_latest_wasm", (individual_canister_id, )); }) }) diff --git a/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_individual_canisters.rs b/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_individual_canisters.rs index b308007a..55bf0868 100644 --- a/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_individual_canisters.rs +++ b/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_individual_canisters.rs @@ -6,7 +6,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] fn start_reclaiming_cycles_from_individual_canisters() -> Result{ CANISTER_DATA.with_borrow(|canister_data| { - canister_data.all_subnet_orchestrator_canisters_list.iter().for_each(|subnet_orchestrator_id| { + canister_data.subnet_orchestrators().iter().for_each(|subnet_orchestrator_id| { ic_cdk::notify(*subnet_orchestrator_id, "reclaim_cycles_from_individual_canisters", ()).unwrap(); }); }); diff --git a/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_subnet_orchestrator_canister.rs b/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_subnet_orchestrator_canister.rs index 94a76be7..e2645f3d 100644 --- a/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_subnet_orchestrator_canister.rs +++ b/src/canister/platform_orchestrator/src/api/cycle_management/start_reclaiming_cycles_from_subnet_orchestrator_canister.rs @@ -7,7 +7,7 @@ use crate::{guard::is_caller::is_caller_global_admin_or_controller, CANISTER_DAT #[update(guard = "is_caller_global_admin_or_controller")] async fn start_reclaiming_cycles_from_subnet_orchestrator_canister() -> String { CANISTER_DATA.with_borrow(|canister_data| { - canister_data.all_subnet_orchestrator_canisters_list.iter().for_each(|subnet_orchestrator_id| { + canister_data.subnet_orchestrators().iter().for_each(|subnet_orchestrator_id| { ic_cdk::notify(*subnet_orchestrator_id, "return_cycles_to_platform_orchestrator_canister", ()).unwrap(); }); }); diff --git a/src/canister/platform_orchestrator/src/data_model/mod.rs b/src/canister/platform_orchestrator/src/data_model/mod.rs index b38605a1..23552ed4 100644 --- a/src/canister/platform_orchestrator/src/data_model/mod.rs +++ b/src/canister/platform_orchestrator/src/data_model/mod.rs @@ -13,7 +13,7 @@ use shared_utils::{ args::UpgradeCanisterArg, well_known_principal::PlatformOrchestratorKnownPrincipal, SubnetUpgradeReport, }, - common::types::wasm::{CanisterWasm, WasmType}, + common::{participant_crypto::{merkle::ChildrenMerkle, ProofOfParticipationDeriverStore}, types::wasm::{CanisterWasm, WasmType}}, }; use self::memory::{ @@ -30,8 +30,8 @@ pub struct StateGuard { #[derive(Serialize, Deserialize)] pub struct CanisterData { - pub all_subnet_orchestrator_canisters_list: HashSet, - pub all_post_cache_orchestrator_list: HashSet, + all_subnet_orchestrator_canisters_list: HashSet, + all_post_cache_orchestrator_list: HashSet, pub subet_orchestrator_with_capacity_left: HashSet, pub version_detail: VersionDetails, #[serde(skip, default = "_default_wasms")] @@ -47,6 +47,8 @@ pub struct CanisterData { pub subnets_upgrade_report: SubnetUpgradeReport, #[serde(default)] pub state_guard: StateGuard, + #[serde(default)] + pub children_merkle: ChildrenMerkle, } fn _default_wasms() -> StableBTreeMap { @@ -75,8 +77,53 @@ impl Default for CanisterData { platform_global_admins: Default::default(), subnets_upgrade_report: SubnetUpgradeReport::default(), state_guard: StateGuard::default(), + children_merkle: ChildrenMerkle::default(), + } + } +} + +impl CanisterData { + pub fn insert_subnet_orchestrator_and_post_cache(&mut self, subnet_orchestrator: Principal, post_cache: Principal) { + self.all_subnet_orchestrator_canisters_list.insert(subnet_orchestrator); + self.all_post_cache_orchestrator_list.insert(post_cache); + self.subet_orchestrator_with_capacity_left.insert(subnet_orchestrator); + self.children_merkle.insert_children([subnet_orchestrator, post_cache]); + } + + pub fn insert_subnet_orchestrator(&mut self, subnet_orchestrator: Principal, provisioning_available: bool) { + self.all_subnet_orchestrator_canisters_list.insert(subnet_orchestrator); + if provisioning_available { + self.subet_orchestrator_with_capacity_left.insert(subnet_orchestrator); + } + self.children_merkle.insert_children([subnet_orchestrator]); + } + + pub fn remove_subnet_orchestrator(&mut self, subnet_orchestrator: Principal, remove_it_completely: bool) { + self.subet_orchestrator_with_capacity_left.remove(&subnet_orchestrator); + if remove_it_completely { + self.all_subnet_orchestrator_canisters_list.remove(&subnet_orchestrator); + // WARN: does not revoke their Proof of Participation + self.children_merkle.remove_child(subnet_orchestrator); } } + + pub fn subnet_orchestrators(&self) -> &HashSet { + &self.all_subnet_orchestrator_canisters_list + } + + pub fn post_cache_orchestrators(&self) -> &HashSet { + &self.all_post_cache_orchestrator_list + } +} + +impl ProofOfParticipationDeriverStore for CanisterData { + fn children_merkle(&self) -> &ChildrenMerkle { + &self.children_merkle + } + + fn children_merkle_mut(&mut self) -> &mut ChildrenMerkle { + &mut self.children_merkle + } } #[derive(Serialize, Deserialize, CandidType, Clone)] diff --git a/src/canister/platform_orchestrator/src/utils/registered_subnet_orchestrator.rs b/src/canister/platform_orchestrator/src/utils/registered_subnet_orchestrator.rs index 27dfe515..6bd6e942 100644 --- a/src/canister/platform_orchestrator/src/utils/registered_subnet_orchestrator.rs +++ b/src/canister/platform_orchestrator/src/utils/registered_subnet_orchestrator.rs @@ -58,7 +58,7 @@ impl RegisteredSubnetOrchestrator { pub fn new(canister_id: Principal) -> Result { let contains = CANISTER_DATA.with_borrow(|canister_data| { canister_data - .all_subnet_orchestrator_canisters_list + .subnet_orchestrators() .contains(&canister_id) }); diff --git a/src/canister/user_index/Cargo.toml b/src/canister/user_index/Cargo.toml index a62a0604..d1bd5d66 100644 --- a/src/canister/user_index/Cargo.toml +++ b/src/canister/user_index/Cargo.toml @@ -22,3 +22,4 @@ futures = { workspace = true } [dev-dependencies] test_utils = { workspace = true } + diff --git a/src/canister/user_index/can.did b/src/canister/user_index/can.did index 77edc0fa..2396d91a 100644 --- a/src/canister/user_index/can.did +++ b/src/canister/user_index/can.did @@ -59,6 +59,13 @@ type KnownPrincipalType = variant { UserIdGlobalSuperAdmin; }; type LogVisibility = variant { controllers; public }; +type ProofOfChild = record { + proof_of_inclusion : vec blob; + "principal" : principal; + children_proof : ProofOfChildren; +}; +type ProofOfChildren = record { signature : blob; merkle_root : blob }; +type ProofOfParticipation = record { chain : vec ProofOfChild }; type QueryStats = record { response_payload_bytes_total : nat; num_instructions_total : nat; @@ -112,6 +119,7 @@ type UserAccessRole = variant { ProjectCanister; }; type UserIndexInitArgs = record { + proof_of_participation : opt ProofOfParticipation; known_principal_ids : opt vec record { KnownPrincipalType; principal }; version : text; access_control_map : opt vec record { principal; vec UserAccessRole }; diff --git a/src/canister/user_index/src/api/canister_lifecycle/init.rs b/src/canister/user_index/src/api/canister_lifecycle/init.rs index 2a4715d3..8f66256b 100644 --- a/src/canister/user_index/src/api/canister_lifecycle/init.rs +++ b/src/canister/user_index/src/api/canister_lifecycle/init.rs @@ -23,6 +23,9 @@ fn init_impl(init_args: UserIndexInitArgs, data: &mut CanisterData) { }); data.allow_upgrades_for_individual_canisters = true; data.last_run_upgrade_status.version = init_args.version; + if let Some(pop) = init_args.proof_of_participation { + data.proof_of_participation = Some(pop); + } } #[cfg(test)] @@ -31,7 +34,7 @@ mod test { use shared_utils::{ access_control::UserAccessRole, - common::types::known_principal::{KnownPrincipalMap, KnownPrincipalType}, + common::{participant_crypto::ProofOfParticipation, types::known_principal::{KnownPrincipalMap, KnownPrincipalType}}, }; use test_utils::setup::test_constants::{ get_global_super_admin_principal_id, @@ -72,6 +75,7 @@ mod test { known_principal_ids: Some(known_principal_ids), access_control_map: Some(access_control_map), version: String::from("v1.0.0"), + proof_of_participation: None, }; let mut data = CanisterData::default(); diff --git a/src/canister/user_index/src/api/canister_lifecycle/post_upgrade.rs b/src/canister/user_index/src/api/canister_lifecycle/post_upgrade.rs index 02a9fc63..9d6ee35b 100644 --- a/src/canister/user_index/src/api/canister_lifecycle/post_upgrade.rs +++ b/src/canister/user_index/src/api/canister_lifecycle/post_upgrade.rs @@ -19,8 +19,8 @@ fn post_upgrade() { fn update_version_from_args() { let (upgrade_args,) = ic_cdk::api::call::arg_data::<(UserIndexInitArgs,)>(ArgDecoderConfig::default()); - CANISTER_DATA.with(|canister_data_ref| { - let last_upgrade_status = canister_data_ref.borrow().last_run_upgrade_status.clone(); + CANISTER_DATA.with_borrow_mut(|canister_data_ref| { + let last_upgrade_status = canister_data_ref.last_run_upgrade_status.clone(); let upgrade_status = UpgradeStatus { last_run_on: system_time::get_current_system_time_from_ic(), failed_canister_ids: vec![], @@ -28,7 +28,10 @@ fn update_version_from_args() { successful_upgrade_count: 0, version: upgrade_args.version, }; - canister_data_ref.borrow_mut().last_run_upgrade_status = upgrade_status; + canister_data_ref.last_run_upgrade_status = upgrade_status; + if let Some(pop) = upgrade_args.proof_of_participation { + canister_data_ref.proof_of_participation = Some(pop); + } }) } diff --git a/src/canister/user_index/src/api/canister_management/allot_empty_canister.rs b/src/canister/user_index/src/api/canister_management/allot_empty_canister.rs index c7b4af22..27bdefdf 100644 --- a/src/canister/user_index/src/api/canister_management/allot_empty_canister.rs +++ b/src/canister/user_index/src/api/canister_management/allot_empty_canister.rs @@ -22,13 +22,13 @@ async fn allot_empty_canister() -> Result { let result = registered_individual_canister.allot_empty_canister().await; let backup_canister_count = - CANISTER_DATA.with_borrow(|canister_data| canister_data.backup_canister_pool.len() as u64); + CANISTER_DATA.with_borrow(|canister_data| canister_data.backup_canisters().len() as u64); if backup_canister_count < get_backup_individual_user_canister_threshold() { let number_of_canisters = get_backup_individual_user_canister_batch_size(); let breaking_condition = || { CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data.backup_canister_pool.len() as u64 + canister_data.backup_canisters().len() as u64 > get_backup_individual_user_canister_batch_size() }) }; diff --git a/src/canister/user_index/src/api/canister_management/create_pool_of_available_canisters.rs b/src/canister/user_index/src/api/canister_management/create_pool_of_available_canisters.rs index b3fdb53b..7d2c0272 100644 --- a/src/canister/user_index/src/api/canister_management/create_pool_of_available_canisters.rs +++ b/src/canister/user_index/src/api/canister_management/create_pool_of_available_canisters.rs @@ -1,16 +1,10 @@ -use std::pin::Pin; - -use futures::Future; +use futures::StreamExt; use ic_cdk::{api::is_controller, caller}; -use shared_utils::{common::{types::wasm::{CanisterWasm, WasmType}, utils::task::run_task_concurrently}, constant::{get_backup_individual_user_canister_batch_size, get_backup_individual_user_canister_threshold, get_individual_user_canister_subnet_batch_size}}; +use shared_utils::{common::types::wasm::{CanisterWasm, WasmType}, constant::{get_backup_individual_user_canister_batch_size, get_backup_individual_user_canister_threshold, get_individual_user_canister_subnet_batch_size}}; use ic_cdk_macros::update; -use crate::{util::canister_management::{create_empty_user_canister, create_users_canister}, CANISTER_DATA}; +use crate::{util::canister_management::{create_empty_user_canister, install_canister_wasm}, CANISTER_DATA}; -enum CanisterCodeState { - Empty, - WasmInstalled -} #[update] pub fn create_pool_of_individual_user_available_canisters(version: String, individual_user_wasm: Vec) -> Result { @@ -35,34 +29,52 @@ pub fn create_pool_of_individual_user_available_canisters(version: String, indiv pub async fn impl_create_pool_of_individual_user_available_canisters(version: String, individual_user_wasm: Vec) { let backup_individual_user_canister_batch_size = get_backup_individual_user_canister_batch_size(); + let individual_user_canister_subnet_batch_size = get_individual_user_canister_subnet_batch_size(); + let total_cnt = backup_individual_user_canister_batch_size + individual_user_canister_subnet_batch_size; //empty canister for backup - let create_empty_canister_futures = (0..backup_individual_user_canister_batch_size) - .map(|_| Box::pin(async { - let canister_id = create_empty_user_canister().await; - (canister_id, CanisterCodeState::Empty) - }) as Pin>>); - - //canisters with installed wasm for available pool - let individual_user_canister_subnet_batch_size = get_individual_user_canister_subnet_batch_size(); - let create_canister_with_wasm_futures = (0..individual_user_canister_subnet_batch_size) - .map(|_| Box::pin(async { - let canister_id = create_users_canister(None, version.clone(), individual_user_wasm.clone()).await; - (canister_id, CanisterCodeState::WasmInstalled) - }) as Pin>>); + let create_empty_canister_futures = (0..total_cnt) + .map(|_| create_empty_user_canister()); + let mut cans_stream = futures::stream::iter(create_empty_canister_futures).buffer_unordered(10); - let combined_create_canister_futures = create_canister_with_wasm_futures.chain(create_empty_canister_futures); + let to_install: Vec<_> = cans_stream.by_ref().take(individual_user_canister_subnet_batch_size as usize).collect().await; + CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.children_merkle.insert_children(to_install.clone()); + }); - run_task_concurrently(combined_create_canister_futures, - 10, - |(canister_id, canister_code_state)| { - CANISTER_DATA.with_borrow_mut(|canister_data| { - match canister_code_state { - CanisterCodeState::Empty => canister_data.backup_canister_pool.insert(canister_id), - CanisterCodeState::WasmInstalled => canister_data.available_canisters.insert(canister_id) - }; - }); - }, - || false).await; + //canisters with installed wasm for available pool + let install_wasm_futs = to_install.into_iter().map(move |canister_id| { + let version = version.clone(); + let individual_user_wasm = individual_user_wasm.clone(); + async move { + install_canister_wasm( + canister_id, + None, + version, + individual_user_wasm, + ).await + } + }); + let mut install_wasm_stream = futures::stream::iter(install_wasm_futs).buffer_unordered(10); + while let Some(res) = install_wasm_stream.next().await { + match res { + Ok(canister_id) => { + CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.available_canisters.insert(canister_id); + }) + } + Err((canister_id, e)) => { + ic_cdk::println!("Failed to install wasm on canister: {:?}, error: {:?}", canister_id, e); + CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.insert_backup_canister(canister_id); + }) + } + } + } + while let Some(canister_id) = cans_stream.next().await { + CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.insert_backup_canister(canister_id); + }) + } } \ No newline at end of file diff --git a/src/canister/user_index/src/api/canister_management/get_subnet_backup_capacity.rs b/src/canister/user_index/src/api/canister_management/get_subnet_backup_capacity.rs index 5d33afa6..dba6de49 100644 --- a/src/canister/user_index/src/api/canister_management/get_subnet_backup_capacity.rs +++ b/src/canister/user_index/src/api/canister_management/get_subnet_backup_capacity.rs @@ -1,4 +1,3 @@ -use std::borrow::Borrow; use ic_cdk_macros::query; use crate::CANISTER_DATA; @@ -7,6 +6,6 @@ use crate::CANISTER_DATA; #[query] pub fn get_subnet_backup_capacity() -> u64 { CANISTER_DATA.with_borrow(|canister_data| { - canister_data.borrow().backup_canister_pool.len() as u64 + canister_data.backup_canisters().len() as u64 }) } \ No newline at end of file diff --git a/src/canister/user_index/src/api/upgrade_individual_user_template/update_user_index_upgrade_user_canisters_with_latest_wasm.rs b/src/canister/user_index/src/api/upgrade_individual_user_template/update_user_index_upgrade_user_canisters_with_latest_wasm.rs index 999d4316..c7ab89ff 100644 --- a/src/canister/user_index/src/api/upgrade_individual_user_template/update_user_index_upgrade_user_canisters_with_latest_wasm.rs +++ b/src/canister/user_index/src/api/upgrade_individual_user_template/update_user_index_upgrade_user_canisters_with_latest_wasm.rs @@ -48,6 +48,13 @@ pub async fn upgrade_user_canisters_with_latest_wasm( }) }); + // TODO: remove this after upgrade is executed on all individual canisters + CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.children_merkle.insert_children( + user_principal_id_to_canister_id_vec.iter().map(|(_, canister_id)| *canister_id) + ); + }); + let saved_upgrade_status = CANISTER_DATA.with(|canister_data_ref_cell| { canister_data_ref_cell .borrow() @@ -178,6 +185,12 @@ async fn upgrade_user_canister( version: String, individual_user_wasm: Vec, ) -> Result<(), String> { + // TODO: remove this after upgrade is executed on all individual canisters + let mut proof_of_participation = CANISTER_DATA.with_borrow(|cdata| cdata.proof_of_participation.clone()); + if let Some(pop) = proof_of_participation { + proof_of_participation = Some(pop.derive_for_child(&CANISTER_DATA, canister_id).await?); + } + canister_management::upgrade_individual_user_canister( canister_id, CanisterInstallMode::Upgrade(None), @@ -187,6 +200,7 @@ async fn upgrade_user_canister( upgrade_version_number: None, url_to_send_canister_metrics_to: None, version, + proof_of_participation, }, individual_user_wasm, ) diff --git a/src/canister/user_index/src/api/upgrade_individual_user_template/upgrade_specific_individual_user_canister_with_latest_wasm.rs b/src/canister/user_index/src/api/upgrade_individual_user_template/upgrade_specific_individual_user_canister_with_latest_wasm.rs index 5789ab58..d63a5e76 100644 --- a/src/canister/user_index/src/api/upgrade_individual_user_template/upgrade_specific_individual_user_canister_with_latest_wasm.rs +++ b/src/canister/user_index/src/api/upgrade_individual_user_template/upgrade_specific_individual_user_canister_with_latest_wasm.rs @@ -47,6 +47,15 @@ async fn upgrade_specific_individual_user_canister_with_latest_wasm( .unwrap() }); + // TODO: remove this after upgrade is executed on all individual canisters + let mut proof_of_participation = CANISTER_DATA.with_borrow_mut(|cdata| { + cdata.children_merkle.insert_children([user_canister_id]); + + cdata.proof_of_participation.clone() + }); + if let Some(pop) = proof_of_participation { + proof_of_participation = Some(pop.derive_for_child(&CANISTER_DATA, user_canister_id).await.unwrap()); + } match canister_management::upgrade_individual_user_canister( user_canister_id, upgrade_mode.unwrap_or(CanisterInstallMode::Upgrade(None)), @@ -56,6 +65,7 @@ async fn upgrade_specific_individual_user_canister_with_latest_wasm( upgrade_version_number: Some(saved_upgrade_status.version_number + 1), url_to_send_canister_metrics_to: Some(configuration.url_to_send_canister_metrics_to), version: individual_canister_wasm.version, + proof_of_participation, }, individual_canister_wasm.wasm_blob, ) diff --git a/src/canister/user_index/src/api/user_record/get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer.rs b/src/canister/user_index/src/api/user_record/get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer.rs index e847fb45..758e9a2b 100644 --- a/src/canister/user_index/src/api/user_record/get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer.rs +++ b/src/canister/user_index/src/api/user_record/get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer.rs @@ -155,7 +155,7 @@ async fn new_user_signup(user_id: Principal) -> Result { let available_individual_user_canisters_cnt = CANISTER_DATA.with_borrow(|canister_data| canister_data.available_canisters.len() as u64); let backup_individual_user_canister_cnt = - CANISTER_DATA.with_borrow(|canister_data| canister_data.backup_canister_pool.len() as u64); + CANISTER_DATA.with_borrow(|canister_data| canister_data.backup_canisters().len() as u64); let total_canister_provisioned_on_subnet = individual_user_canisters_cnt + available_individual_user_canisters_cnt + backup_individual_user_canister_cnt; @@ -210,7 +210,7 @@ async fn new_user_signup(user_id: Principal) -> Result { async fn provision_new_available_canisters(individual_user_template_canister_wasm: CanisterWasm) { let backup_pool_canister_count = - CANISTER_DATA.with_borrow(|canister_data| canister_data.backup_canister_pool.len() as u64); + CANISTER_DATA.with_borrow(|canister_data| canister_data.backup_canisters().len() as u64); let individual_canister_batch_size = get_individual_user_canister_subnet_batch_size(); let canister_count = individual_canister_batch_size.min(backup_pool_canister_count); let available_canister_count = @@ -218,7 +218,7 @@ async fn provision_new_available_canisters(individual_user_template_canister_was let max_canister_count = available_canister_count + canister_count; let install_canister_wasm_futures = CANISTER_DATA.with_borrow(|canister_data| { - let mut backup_pool_canister = canister_data.backup_canister_pool.clone().into_iter(); + let mut backup_pool_canister = canister_data.backup_canisters().clone().into_iter(); (0..canister_count).map(move |_| { let individual_user_template_canister_wasm_version = individual_user_template_canister_wasm.version.clone(); @@ -230,7 +230,7 @@ async fn provision_new_available_canisters(individual_user_template_canister_was // Remove the canister id from backup pool so no one else access it CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data.backup_canister_pool.remove(&canister_id) + canister_data.remove_backup_canister(&canister_id) }); async move { @@ -254,7 +254,7 @@ async fn provision_new_available_canisters(individual_user_template_canister_was canister_data.available_canisters.insert(canister_id); } Err(e) => { - canister_data.backup_canister_pool.insert(e.0); + canister_data.reinsert_backup_canister_due_to_failure(e.0); ic_cdk::println!("Error installing wasm for canister {} {}", e.0, e.1); } }) @@ -293,7 +293,7 @@ async fn recharge_empty_canister_if_required(canister_id: Principal) { async fn provision_new_backup_canisters(canister_count: u64) { let breaking_condition = || { CANISTER_DATA.with_borrow(|canister_data| { - canister_data.backup_canister_pool.len() as u64 + canister_data.backup_canisters().len() as u64 > get_backup_individual_user_canister_batch_size() }) }; diff --git a/src/canister/user_index/src/api/user_record/issue_rewards_for_referral.rs b/src/canister/user_index/src/api/user_record/issue_rewards_for_referral.rs index ec2a874a..ef1a09bf 100644 --- a/src/canister/user_index/src/api/user_record/issue_rewards_for_referral.rs +++ b/src/canister/user_index/src/api/user_record/issue_rewards_for_referral.rs @@ -4,6 +4,7 @@ use ic_cdk_macros::update; use shared_utils::{canister_specific::individual_user_template::types::session::SessionType, common::utils::permissions::is_caller_global_admin}; +#[deprecated = "use new referral methods in individual user canister"] #[update(guard = "is_caller_global_admin")] pub async fn issue_rewards_for_referral(user_canister_id: Principal, referrer_principal: Principal, referee_principal: Principal) -> Result { diff --git a/src/canister/user_index/src/api/user_signup/are_signups_enabled.rs b/src/canister/user_index/src/api/user_signup/are_signups_enabled.rs index 4e6aad73..eb391238 100644 --- a/src/canister/user_index/src/api/user_signup/are_signups_enabled.rs +++ b/src/canister/user_index/src/api/user_signup/are_signups_enabled.rs @@ -25,15 +25,13 @@ mod test { #[test] fn test_are_signups_enabled_impl() { - let mut canister_data = CanisterData { - configuration: Configuration { + let mut canister_data = CanisterData::default(); + canister_data.configuration = Configuration { known_principal_ids: HashMap::default(), signups_open_on_this_subnet: true, url_to_send_canister_metrics_to: String::from("http://example.com") - }, - ..Default::default() }; - + assert!(are_signups_enabled_impl(&canister_data)); canister_data.configuration.signups_open_on_this_subnet = false; diff --git a/src/canister/user_index/src/data_model/mod.rs b/src/canister/user_index/src/data_model/mod.rs index ba9589c8..3b0ceaa3 100644 --- a/src/canister/user_index/src/data_model/mod.rs +++ b/src/canister/user_index/src/data_model/mod.rs @@ -6,6 +6,8 @@ use serde::Serialize; use shared_utils::canister_specific::user_index::types::{ BroadcastCallStatus, RecycleStatus, UpgradeStatus, }; +use shared_utils::common::participant_crypto::merkle::ChildrenMerkle; +use shared_utils::common::participant_crypto::{ProofOfParticipation, ProofOfParticipationDeriverStore}; use shared_utils::common::types::wasm::{CanisterWasm, WasmType}; use self::memory::get_wasm_memory; @@ -29,7 +31,7 @@ pub struct CanisterData { pub allow_upgrades_for_individual_canisters: bool, pub available_canisters: HashSet, #[serde(default)] - pub backup_canister_pool: HashSet, + backup_canister_pool: HashSet, pub user_principal_id_to_canister_id_map: BTreeMap, pub unique_user_name_to_user_principal_id_map: BTreeMap, #[serde(skip, default = "_empty_wasms")] @@ -38,6 +40,10 @@ pub struct CanisterData { pub recycle_status: RecycleStatus, #[serde(default)] pub last_broadcast_call_status: BroadcastCallStatus, + #[serde(default)] + pub proof_of_participation: Option, + #[serde(default)] + pub children_merkle: ChildrenMerkle, } impl Default for CanisterData { @@ -53,10 +59,50 @@ impl Default for CanisterData { backup_canister_pool: Default::default(), recycle_status: Default::default(), last_broadcast_call_status: Default::default(), + proof_of_participation: None, + children_merkle: ChildrenMerkle::default(), } } } +impl CanisterData { + pub fn insert_backup_canister(&mut self, canister_id: Principal) -> bool { + let inserted = self.backup_canister_pool.insert(canister_id); + if inserted { + self.children_merkle.insert_children([canister_id]); + } + inserted + } + + pub fn remove_backup_canister(&mut self, canister_id: &Principal) { + self.backup_canister_pool.remove(&canister_id); + // removal from backup pool does not mean its not part of our fleet + // an individual canister might be installed on it instead, for example + // so we don't remove it from children_merkle + } + + // reinsert a previously removed backup canister + // caller MUST be careful and only call this function if the canister was previously removed + // USE [`CanisterData::insert_backup_canister`] if you are not sure + pub fn reinsert_backup_canister_due_to_failure(&mut self, canister_id: Principal) { + self.backup_canister_pool.insert(canister_id); + } + + pub fn backup_canisters(&self) -> &HashSet { + &self.backup_canister_pool + } +} + +impl ProofOfParticipationDeriverStore for CanisterData { + fn children_merkle(&self) -> &ChildrenMerkle { + &self.children_merkle + } + + fn children_merkle_mut(&mut self) -> &mut ChildrenMerkle { + &mut self.children_merkle + } +} + fn _empty_wasms() -> StableBTreeMap { StableBTreeMap::init(get_wasm_memory()) } diff --git a/src/canister/user_index/src/util/canister_management.rs b/src/canister/user_index/src/util/canister_management.rs index 3028dc2a..51b0c96b 100644 --- a/src/canister/user_index/src/util/canister_management.rs +++ b/src/canister/user_index/src/util/canister_management.rs @@ -48,16 +48,6 @@ struct CustomInstallCodeArgument { pub unsafe_drop_stable_memory: Option, } -pub async fn create_users_canister( - profile_owner: Option, - version: String, - individual_user_wasm: Vec, -) -> Principal { - let canister_id = create_empty_user_canister().await; - install_canister_wasm(canister_id, profile_owner, version, individual_user_wasm).await; - canister_id -} - pub async fn create_empty_user_canister() -> Principal { // * config for provisioning canister let arg = CreateCanisterArgument { @@ -93,7 +83,7 @@ pub async fn provision_number_of_empty_canisters( let result_callback = |canister_id: Principal| { CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data.backup_canister_pool.insert(canister_id) + canister_data.insert_backup_canister(canister_id) }); }; @@ -115,6 +105,10 @@ pub async fn install_canister_wasm( let configuration = CANISTER_DATA .with(|canister_data_ref_cell| canister_data_ref_cell.borrow().configuration.clone()); + let mut proof_of_participation = CANISTER_DATA.with_borrow(|cdata| cdata.proof_of_participation.clone()); + if let Some(pop) = proof_of_participation { + proof_of_participation = Some(pop.derive_for_child(&CANISTER_DATA, canister_id).await.unwrap()); + } let individual_user_tempalate_init_args = IndividualUserTemplateInitArgs { profile_owner, known_principal_ids: Some(CANISTER_DATA.with(|canister_data_ref_cell| { @@ -127,6 +121,7 @@ pub async fn install_canister_wasm( upgrade_version_number: Some(0), version, url_to_send_canister_metrics_to: Some(configuration.url_to_send_canister_metrics_to), + proof_of_participation, }; // * encode argument for user canister init lifecycle method @@ -155,6 +150,10 @@ pub async fn reinstall_canister_wasm( let configuration = CANISTER_DATA .with(|canister_data_ref_cell| canister_data_ref_cell.borrow().configuration.clone()); + let mut proof_of_participation = CANISTER_DATA.with_borrow(|cdata| cdata.proof_of_participation.clone()); + if let Some(pop) = proof_of_participation { + proof_of_participation = Some(pop.derive_for_child(&CANISTER_DATA, canister_id).await?); + } let individual_user_tempalate_init_args = IndividualUserTemplateInitArgs { profile_owner, known_principal_ids: Some(CANISTER_DATA.with(|canister_data_ref_cell| { @@ -167,6 +166,7 @@ pub async fn reinstall_canister_wasm( upgrade_version_number: Some(0), version, url_to_send_canister_metrics_to: Some(configuration.url_to_send_canister_metrics_to), + proof_of_participation, }; // * encode argument for user canister init lifecycle method diff --git a/src/canister/user_index/src/util/types/individual_user_canister.rs b/src/canister/user_index/src/util/types/individual_user_canister.rs index 42f63e2d..2e29ee17 100644 --- a/src/canister/user_index/src/util/types/individual_user_canister.rs +++ b/src/canister/user_index/src/util/types/individual_user_canister.rs @@ -96,12 +96,12 @@ impl IndividualUserCanister { pub async fn allot_empty_canister(&self) -> Result { let alloted_canister_id_res = CANISTER_DATA.with_borrow_mut(|canister_data| { - let Some(new_canister_id) = canister_data.backup_canister_pool.iter().next().copied() + let Some(new_canister_id) = canister_data.backup_canisters().iter().next().copied() else { return Err("No Backup Canisters Available".into()); }; - canister_data.backup_canister_pool.remove(&new_canister_id); + canister_data.remove_backup_canister(&new_canister_id); Ok(new_canister_id) }); @@ -118,7 +118,7 @@ impl IndividualUserCanister { .await .inspect_err(|_| { CANISTER_DATA.with_borrow_mut(|canister_data| { - canister_data.backup_canister_pool.insert(canister_id) + canister_data.reinsert_backup_canister_due_to_failure(canister_id) }); }) .map_err(|e| e.1)?; diff --git a/src/lib/integration_tests/tests/authentication/when_a_new_user_signs_up_from_a_referral_then_the_new_user_is_given_a_thousand_utility_tokens_for_signing_up_and_the_referrer_and_referee_receive_five_hundred_tokens_as_referral_rewards.rs b/src/lib/integration_tests/tests/authentication/when_a_new_user_signs_up_from_a_referral_then_the_new_user_is_given_a_thousand_utility_tokens_for_signing_up_and_the_referrer_and_referee_receive_five_hundred_tokens_as_referral_rewards.rs index 0e26990f..0a25a7be 100644 --- a/src/lib/integration_tests/tests/authentication/when_a_new_user_signs_up_from_a_referral_then_the_new_user_is_given_a_thousand_utility_tokens_for_signing_up_and_the_referrer_and_referee_receive_five_hundred_tokens_as_referral_rewards.rs +++ b/src/lib/integration_tests/tests/authentication/when_a_new_user_signs_up_from_a_referral_then_the_new_user_is_given_a_thousand_utility_tokens_for_signing_up_and_the_referrer_and_referee_receive_five_hundred_tokens_as_referral_rewards.rs @@ -13,7 +13,7 @@ use shared_utils::{ types::canister_specific::individual_user_template::error_types::GetUserUtilityTokenTransactionHistoryError, }; use test_utils::setup::{ - env::v1::{get_initialized_env_with_provisioned_known_canisters, get_new_state_machine}, + env::{pocket_ic_env::{execute_query, execute_query_multi, execute_update, execute_update_no_res, get_new_pocket_ic_env}, v1::{get_initialized_env_with_provisioned_known_canisters, get_new_state_machine}}, test_constants::{get_mock_user_alice_principal_id, get_mock_user_bob_principal_id}, }; @@ -466,3 +466,185 @@ fn when_a_new_user_signs_up_from_a_referral_then_the_new_user_is_given_a_thousan alice_utility_token_transaction_history_after_referral ); } + +#[test] +fn when_a_new_user_signs_up_from_a_referral_then_the_new_user_is_given_a_thousand_utility_tokens_for_signing_up_and_the_referrer_and_referee_receive_five_hundred_tokens_as_referral_rewards_v2() { + let (pic, known_principals) = get_new_pocket_ic_env(); + let global_admin_principal = known_principals[&KnownPrincipalType::UserIdGlobalSuperAdmin]; + + let platform_orc = known_principals[&KnownPrincipalType::CanisterIdPlatformOrchestrator]; + let app_subnets = pic.topology().get_app_subnets(); + let user_index_res: Result = execute_update( + &pic, + global_admin_principal, + platform_orc, + "provision_subnet_orchestrator_canister", + &app_subnets[0] + ); + let user_index = user_index_res.unwrap(); + for _ in 0..30 { + pic.tick(); + } + + let alice_principal = get_mock_user_alice_principal_id(); + let alice_canister: Principal = execute_update( + &pic, + alice_principal, + user_index, + "get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer", + &() + ); + + let alice_balance_after_signup: u64 = execute_query( + &pic, + Principal::anonymous(), + alice_canister, + "get_utility_token_balance", + &() + ); + assert_eq!(alice_balance_after_signup, 1000); + + let alice_txn_history_after_signup_res: Result, GetUserUtilityTokenTransactionHistoryError> = execute_query_multi( + &pic, + Principal::anonymous(), + alice_canister, + "get_user_utility_token_transaction_history_with_pagination", + (0u64, 10u64) + ); + let alice_txn_history_after_signup = alice_txn_history_after_signup_res.unwrap(); + + assert!(matches!( + alice_txn_history_after_signup.as_slice(), + [ + (_, TokenEvent::Mint { + details: MintEvent::NewUserSignup { + new_user_principal_id + }, + .. + }) + ] if *new_user_principal_id == alice_principal + )); + + let super_admin_user_id = Principal::from_text(GLOBAL_SUPER_ADMIN_USER_ID).unwrap(); + execute_update_no_res( + &pic, + super_admin_user_id, + alice_canister, + "update_session_type", + &SessionType::RegisteredSession + ); + + let bob_principal = get_mock_user_bob_principal_id(); + let bob_canister: Principal = execute_update( + &pic, + bob_principal, + user_index, + "get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer", + &() + ); + + let ref_details = UserCanisterDetails { + profile_owner: alice_principal, + user_canister_id: alice_canister, + }; + execute_update_no_res( + &pic, + bob_principal, + bob_canister, + "update_referrer_details", + &ref_details + ); + + let bob_profile_details: UserProfileDetailsForFrontend = execute_query( + &pic, + Principal::anonymous(), + bob_canister, + "get_profile_details", + &() + ); + assert!(matches!( + bob_profile_details.referrer_details, + Some(details) if details == ref_details + )); + + execute_update_no_res( + &pic, + super_admin_user_id, + bob_canister, + "update_session_type", + &SessionType::RegisteredSession + ); + + execute_update_no_res( + &pic, + bob_principal, + bob_canister, + "receive_reward_for_being_referred", + &() + ); + + // wait for receive_reward to trigger on alice as well + for _ in 0..30 { + pic.tick(); + } + + let alice_balance_after_ref: u64 = execute_query( + &pic, + Principal::anonymous(), + alice_canister, + "get_utility_token_balance", + &() + ); + assert_eq!(alice_balance_after_ref, 1500); + + let bob_balance_after_ref: u64 = execute_query( + &pic, + Principal::anonymous(), + alice_canister, + "get_utility_token_balance", + &() + ); + assert_eq!(bob_balance_after_ref, 1500); + + let alice_txn_history_after_ref_res: Result, GetUserUtilityTokenTransactionHistoryError> = execute_query_multi( + &pic, + Principal::anonymous(), + alice_canister, + "get_user_utility_token_transaction_history_with_pagination", + (0u64, 10u64) + ); + let alice_txn_history_after_ref = alice_txn_history_after_ref_res.unwrap(); + assert_eq!(alice_txn_history_after_ref.len(), 2); + + assert!(matches!( + alice_txn_history_after_ref[0].1, + TokenEvent::Mint { + details: MintEvent::Referral { + referee_user_principal_id, + referrer_user_principal_id + }, + .. + } if referrer_user_principal_id == alice_principal && referee_user_principal_id == bob_principal + )); + + let bob_txn_history_after_ref_res: Result, GetUserUtilityTokenTransactionHistoryError> = execute_query_multi( + &pic, + Principal::anonymous(), + bob_canister, + "get_user_utility_token_transaction_history_with_pagination", + (0u64, 10u64) + ); + let bob_txn_history_after_ref = bob_txn_history_after_ref_res.unwrap(); + assert_eq!(bob_txn_history_after_ref.len(), 2); + + assert!(matches!( + bob_txn_history_after_ref[0].1, + TokenEvent::Mint { + details: MintEvent::Referral { + referee_user_principal_id, + referrer_user_principal_id + }, + .. + } if referrer_user_principal_id == alice_principal && referee_user_principal_id == bob_principal + )); +} diff --git a/src/lib/integration_tests/tests/creator_dao/airdrop.rs b/src/lib/integration_tests/tests/creator_dao/airdrop.rs new file mode 100644 index 00000000..f5fa996c --- /dev/null +++ b/src/lib/integration_tests/tests/creator_dao/airdrop.rs @@ -0,0 +1,117 @@ +use candid::{Nat, Principal}; +use test_utils::setup::{env::pocket_ic_env::execute_query, test_constants::{get_mock_user_alice_principal_id, get_mock_user_bob_principal_id, get_mock_user_charlie_principal_id, get_mock_user_dan_principal_id, get_mock_user_tom_principal_id}}; + +use crate::{tokens_to_e8s, CDaoHarness}; + + +#[test] +fn cdao_airdrop_test_with_referral_height_3() { + let harness = CDaoHarness::init(); + + let alice_principal = get_mock_user_alice_principal_id(); + let bob_principal = get_mock_user_bob_principal_id(); + let charlie_principal = get_mock_user_charlie_principal_id(); + let dan_principal = get_mock_user_dan_principal_id(); + let tom_principal = get_mock_user_tom_principal_id(); + + let alice = harness.provision_individual_canister(alice_principal, None); + let atoken = harness.create_new_token(alice.clone()); + let mut alice_can_atoken_bal = harness.icrc1_balance(atoken.ledger, alice.user_canister_id); + let tx_fee: Nat = execute_query( + &harness.pic, + Principal::anonymous(), + atoken.ledger, + "icrc1_fee", + &() + ); + + let bob = harness.provision_individual_canister(bob_principal, Some(alice.clone())); + let _charlie = harness.provision_individual_canister(charlie_principal, Some(alice.clone())); + // wait for referral rewards to spread + for _ in 0..20 { + harness.pic.tick(); + } + alice_can_atoken_bal -= (tokens_to_e8s(1) + tx_fee.clone()) * 2u32; + + let bob_atoken_bal = harness.icrc1_balance(atoken.ledger, bob_principal); + let charlie_atoken_bal = harness.icrc1_balance(atoken.ledger, charlie_principal); + assert_eq!(bob_atoken_bal, tokens_to_e8s(1)); + assert_eq!(charlie_atoken_bal, tokens_to_e8s(1)); + + let alice_can_atoken_bal_after = harness.icrc1_balance(atoken.ledger, alice.user_canister_id); + assert_eq!(alice_can_atoken_bal_after, alice_can_atoken_bal); + + let btoken = harness.create_new_token(bob.clone()); + for _ in 0..20 { + harness.pic.tick(); + } + + let alice_btoken_bal = harness.icrc1_balance(btoken.ledger, alice_principal); + assert_eq!(alice_btoken_bal, tokens_to_e8s(1)); + + let mut bob_can_btoken_bal = harness.icrc1_balance(btoken.ledger, bob.user_canister_id); + + let dan = harness.provision_individual_canister(dan_principal, Some(bob.clone())); + let tom = harness.provision_individual_canister(tom_principal, Some(bob.clone())); + for _ in 0..30 { + harness.pic.tick(); + } + bob_can_btoken_bal -= (tokens_to_e8s(1) + tx_fee.clone()) * 2u32; + alice_can_atoken_bal -= (tokens_to_e8s(1) + tx_fee.clone()) * 2u32; + + let dan_atoken_bal = harness.icrc1_balance(atoken.ledger, dan_principal); + let tom_atoken_bal = harness.icrc1_balance(atoken.ledger, tom_principal); + let dan_btoken_bal = harness.icrc1_balance(btoken.ledger, dan_principal); + let tom_btoken_bal = harness.icrc1_balance(btoken.ledger, tom_principal); + + assert_eq!(dan_atoken_bal, tokens_to_e8s(1)); + assert_eq!(tom_atoken_bal, tokens_to_e8s(1)); + assert_eq!(dan_btoken_bal, tokens_to_e8s(1)); + assert_eq!(tom_btoken_bal, tokens_to_e8s(1)); + + let alice_can_atoken_bal_after = harness.icrc1_balance(atoken.ledger, alice.user_canister_id); + let bob_can_btoken_bal_after = harness.icrc1_balance(btoken.ledger, bob.user_canister_id); + assert_eq!(alice_can_atoken_bal_after, alice_can_atoken_bal); + assert_eq!(bob_can_btoken_bal_after, bob_can_btoken_bal); + + let a2token = harness.create_new_token(alice); + for _ in 0..30 { + harness.pic.tick(); + } + + let bob_a2token_bal = harness.icrc1_balance(a2token.ledger, bob_principal); + let charlie_a2token_bal = harness.icrc1_balance(a2token.ledger, charlie_principal); + let dan_a2token_bal = harness.icrc1_balance(a2token.ledger, dan_principal); + let tom_a2token_bal = harness.icrc1_balance(a2token.ledger, tom_principal); + + assert_eq!(bob_a2token_bal, tokens_to_e8s(1)); + assert_eq!(charlie_a2token_bal, tokens_to_e8s(1)); + assert_eq!(dan_a2token_bal, tokens_to_e8s(1)); + assert_eq!(tom_a2token_bal, tokens_to_e8s(1)); + + let dtoken = harness.create_new_token(dan.clone()); + let ttoken = harness.create_new_token(tom.clone()); + for _ in 0..30 { + harness.pic.tick(); + } + + let bob_dtoken_bal = harness.icrc1_balance(dtoken.ledger, bob_principal); + let charlie_dtoken_bal = harness.icrc1_balance(dtoken.ledger, charlie_principal); + let tom_dtoken_bal = harness.icrc1_balance(dtoken.ledger, tom_principal); + let alice_dtoken_bal = harness.icrc1_balance(dtoken.ledger, alice_principal); + + let bob_ttoken_bal = harness.icrc1_balance(ttoken.ledger, bob_principal); + let charlie_ttoken_bal = harness.icrc1_balance(ttoken.ledger, charlie_principal); + let dan_ttoken_bal = harness.icrc1_balance(ttoken.ledger, dan_principal); + let alice_ttoken_bal = harness.icrc1_balance(ttoken.ledger, alice_principal); + + assert_eq!(bob_dtoken_bal, tokens_to_e8s(1)); + assert_eq!(alice_dtoken_bal, tokens_to_e8s(1)); + assert_eq!(bob_ttoken_bal, tokens_to_e8s(1)); + assert_eq!(alice_ttoken_bal, tokens_to_e8s(1)); + + assert_eq!(charlie_dtoken_bal, 0u32); + assert_eq!(tom_dtoken_bal, 0u32); + assert_eq!(charlie_ttoken_bal, 0u32); + assert_eq!(dan_ttoken_bal, 0u32); +} \ No newline at end of file diff --git a/src/lib/integration_tests/tests/creator_dao/main.rs b/src/lib/integration_tests/tests/creator_dao/main.rs index 78b31642..59856753 100644 --- a/src/lib/integration_tests/tests/creator_dao/main.rs +++ b/src/lib/integration_tests/tests/creator_dao/main.rs @@ -1,8 +1,8 @@ pub mod types; +pub mod airdrop; use ic_sns_governance::pb::v1::{ - manage_neuron, neuron, Account, ListNeurons, ListNeuronsResponse, ManageNeuron, - ManageNeuronResponse, + manage_neuron, neuron, Account, GetMode, GetModeResponse, ListNeurons, ListNeuronsResponse, ManageNeuron, ManageNeuronResponse }; use ic_sns_init::pb::v1::{ sns_init_payload::InitialTokenDistribution, AirdropDistribution, DeveloperDistribution, @@ -10,18 +10,21 @@ use ic_sns_init::pb::v1::{ TreasuryDistribution, }; use ic_sns_swap::pb::v1::{ - GetInitRequest, GetInitResponse, NeuronBasketConstructionParameters, NewSaleTicketRequest, - NewSaleTicketResponse, RefreshBuyerTokensRequest, RefreshBuyerTokensResponse, + new_sale_ticket_response, NeuronBasketConstructionParameters, NewSaleTicketRequest, NewSaleTicketResponse, RefreshBuyerTokensRequest, }; use sha2::{Digest, Sha256}; +use shared_utils::canister_specific::individual_user_template::types::profile::UserCanisterDetails; +use shared_utils::canister_specific::individual_user_template::types::session::SessionType; +use shared_utils::constant::GLOBAL_SUPER_ADMIN_USER_ID; +use test_utils::setup::env::pocket_ic_env::{execute_query, execute_update, execute_update_multi, execute_update_no_res, execute_update_no_res_multi}; use std::time::{Duration, UNIX_EPOCH}; use std::{collections::HashMap, fmt::Debug, str::FromStr, time::SystemTime, vec}; -use candid::{CandidType, Decode, Encode, Nat, Principal}; +use candid::{CandidType, Encode, Nat, Principal}; use ic_base_types::PrincipalId; use ic_sns_wasm::init::SnsWasmCanisterInitPayload; use icp_ledger::Subaccount; -use pocket_ic::WasmResult; +use pocket_ic::PocketIc; use serde::{Deserialize, Serialize}; use shared_utils::{ canister_specific::individual_user_template::{ @@ -38,8 +41,8 @@ use test_utils::setup::{ }, }; -pub const ICP_LEDGER_CANISTER_ID: &'static str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; -pub const ICP_INDEX_CANISTER_ID: &'static str = "qhbym-qaaaa-aaaaa-aaafq-cai"; +pub const ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; +pub const ICP_INDEX_CANISTER_ID: &str = "qhbym-qaaaa-aaaaa-aaafq-cai"; #[derive(CandidType, Deserialize, PartialEq, Eq, Hash, Serialize, Clone)] struct Wasm { @@ -79,7 +82,11 @@ pub struct ErrorRecord { pub message: String, } -fn add_wasm(wasm_file: &[u8], canister_type: u32) -> AddWasmPayload { +fn tokens_to_e8s(tokens: u64) -> Nat { + Nat::from(tokens) * 1e8 as u64 +} + +fn add_wasm(pic: &PocketIc, wasm_canister: Principal, wasm_file: &[u8], canister_type: u32) { let mut hasher = Sha256::new(); hasher.update(wasm_file); let file_hash = hasher.finalize(); @@ -100,631 +107,488 @@ fn add_wasm(wasm_file: &[u8], canister_type: u32) -> AddWasmPayload { hex::encode(file_hash) ); - wasm_data -} + let super_admin = get_global_super_admin_principal_id(); -#[test] -fn creator_dao_tests() { - let (pocket_ic, known_principal) = get_new_pocket_ic_env(); - let platform_canister_id = known_principal - .get(&KnownPrincipalType::CanisterIdPlatformOrchestrator) - .cloned() - .unwrap(); + let res: AddWasmResultRecord = execute_update( + pic, + super_admin, + wasm_canister, + "add_wasm", + &wasm_data, + ); - let super_admin = get_global_super_admin_principal_id(); + assert!(matches!(res, AddWasmResultRecord { result: Some(ResultVariant::Hash(_)) })); +} + +struct CDaoHarness { + pub pic: PocketIc, + pub subnet_orchestrator: Principal, +} - let application_subnets = pocket_ic.topology().get_app_subnets(); +impl CDaoHarness { + pub fn init() -> Self { + let (pic, known_principal) = get_new_pocket_ic_env(); + let platform_canister_id = known_principal + .get(&KnownPrincipalType::CanisterIdPlatformOrchestrator) + .cloned() + .unwrap(); - let charlie_global_admin = get_mock_user_charlie_principal_id(); + let super_admin = get_global_super_admin_principal_id(); + let application_subnets = pic.topology().get_app_subnets(); + let charlie_global_admin = get_mock_user_charlie_principal_id(); - pocket_ic - .update_call( - platform_canister_id, + execute_update_no_res( + &pic, super_admin, + platform_canister_id, "add_principal_as_global_admin", - candid::encode_one(charlie_global_admin).unwrap(), - ) - .unwrap(); + &charlie_global_admin + ); - pocket_ic - .update_call( - platform_canister_id, + execute_update_no_res_multi( + &pic, super_admin, + platform_canister_id, "update_global_known_principal", - candid::encode_args(( + ( KnownPrincipalType::CanisterIdSnsWasm, - Principal::from_text(SNS_WASM_W_PRINCIPAL_ID).unwrap(), - )) - .unwrap(), - ) - .unwrap(); + Principal::from_text(SNS_WASM_W_PRINCIPAL_ID).unwrap() + ) + ); - let subnet_orchestrator_canister_id: Principal = pocket_ic - .update_call( - platform_canister_id, + let subnet_orchestrator: Principal = execute_update::<_, Result<_, String>>( + &pic, charlie_global_admin, + platform_canister_id, "provision_subnet_orchestrator_canister", - candid::encode_one(application_subnets[1]).unwrap(), - ) - .map(|res| { - let canister_id_result: Result = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("Canister call failed"), - }; - canister_id_result.unwrap() - }) - .unwrap(); - - for i in 0..50 { - pocket_ic.tick(); - } + &application_subnets[1] + ).unwrap(); - let alice_principal = get_mock_user_alice_principal_id(); - let alice_canister_id: Principal = pocket_ic - .update_call( - subnet_orchestrator_canister_id, - alice_principal, - "get_requester_principals_canister_id_create_if_not_exists", - candid::encode_one(()).unwrap(), - ) - .map(|reply_payload| { - let response: Result = match reply_payload { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap() - .unwrap(); - - let alice_initial_cycle_balance = pocket_ic.cycle_balance(alice_canister_id); - - let sns_wasm_w_canister_wasm = include_bytes!("../../../../../wasms/sns-wasm-canister.wasm"); - let sns_wasm_w_canister_id = Principal::from_text(SNS_WASM_W_PRINCIPAL_ID).unwrap(); - - let _ = pocket_ic.create_canister_with_id( - Some(super_admin), - None, - Principal::from_text(SNS_WASM_W_PRINCIPAL_ID).unwrap(), - ); + for _ in 0..1000 { + pic.tick(); + } - let sns_wasm_canister_init_payload = SnsWasmCanisterInitPayload { - sns_subnet_ids: vec![], - access_controls_enabled: false, - allowed_principals: vec![], - }; + let sns_wasm_w_canister_wasm = include_bytes!("../../../../../wasms/sns-wasm-canister.wasm"); + let sns_wasm_w_canister_id = Principal::from_text(SNS_WASM_W_PRINCIPAL_ID).unwrap(); - pocket_ic.install_canister( - sns_wasm_w_canister_id, - sns_wasm_w_canister_wasm.to_vec(), - Encode!(&sns_wasm_canister_init_payload).unwrap(), - Some(super_admin), - ); + let res_principal = pic.create_canister_with_id( + Some(super_admin), + None, + sns_wasm_w_canister_id + ).unwrap(); - let res = pocket_ic - .update_call( + assert_eq!(res_principal, sns_wasm_w_canister_id); + + let sns_wasm_init = SnsWasmCanisterInitPayload { + sns_subnet_ids: vec![], + access_controls_enabled: false, + allowed_principals: vec![] + }; + + pic.install_canister( sns_wasm_w_canister_id, - super_admin, - "add_wasm", - candid::encode_one(add_wasm( - include_bytes!("../../../../../wasms/root.wasm.gz"), - 1, - )) - .unwrap(), - ) - .map(|res| { - let response: AddWasmResultRecord = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( + sns_wasm_w_canister_wasm.to_vec(), + Encode!(&sns_wasm_init).unwrap(), + Some(super_admin), + ); + + add_wasm( + &pic, sns_wasm_w_canister_id, - super_admin, - "add_wasm", - candid::encode_one(add_wasm( - include_bytes!("../../../../../wasms/governance.wasm.gz"), - 2, - )) - .unwrap(), - ) - .map(|res| { - let response: AddWasmResultRecord = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( + include_bytes!("../../../../../wasms/root.wasm.gz"), + 1, + ); + + add_wasm( + &pic, sns_wasm_w_canister_id, - super_admin, - "add_wasm", - candid::encode_one(add_wasm( - include_bytes!("../../../../../wasms/ledger.wasm.gz"), - 3, - )) - .unwrap(), - ) - .map(|res| { - let response: AddWasmResultRecord = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( + include_bytes!("../../../../../wasms/governance.wasm.gz"), + 2, + ); + + add_wasm( + &pic, sns_wasm_w_canister_id, - super_admin, - "add_wasm", - candid::encode_one(add_wasm( - include_bytes!("../../../../../wasms/swap.wasm.gz"), - 4, - )) - .unwrap(), - ) - .map(|res| { - let response: AddWasmResultRecord = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( + include_bytes!("../../../../../wasms/ledger.wasm.gz"), + 3, + ); + + add_wasm( + &pic, sns_wasm_w_canister_id, - super_admin, - "add_wasm", - candid::encode_one(add_wasm( - include_bytes!("../../../../../wasms/archive.wasm.gz"), - 5, - )) - .unwrap(), - ) - .map(|res| { - let response: AddWasmResultRecord = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( + include_bytes!("../../../../../wasms/swap.wasm.gz"), + 4, + ); + + add_wasm( + &pic, sns_wasm_w_canister_id, - super_admin, - "add_wasm", - candid::encode_one(add_wasm( - include_bytes!("../../../../../wasms/index.wasm.gz"), - 6, - )) - .unwrap(), - ) - .map(|res| { - let response: AddWasmResultRecord = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - for _ in 0..50 { - pocket_ic.tick(); - } + include_bytes!("../../../../../wasms/archive.wasm.gz"), + 5, + ); - let res = pocket_ic - .update_call( + add_wasm( + &pic, sns_wasm_w_canister_id, - Principal::anonymous(), - "get_latest_sns_version_pretty".into(), - candid::encode_one(()).unwrap(), - ) - .map(|res| { - let response: HashMap = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - - ic_cdk::println!("๐Ÿงช HASHMAP {:?}", res); - assert_eq!(res.len(), 6); - let start = SystemTime::now(); - - let tx_fee = 1u64; - - let sns_init_args = SnsInitPayload { - confirmation_text: Some("GET RICH QUICK".to_string()), - transaction_fee_e8s: Some(tx_fee), - token_name: Some("Simulation Governance".to_string()), - token_symbol: Some("SIMG".to_string()), - proposal_reject_cost_e8s: Some(1u64), - neuron_minimum_stake_e8s: Some(2u64), - fallback_controller_principal_ids: vec![super_admin.to_string().clone()], - logo: Some("data:image/png;base64,iVBORw0".to_string()), - url: Some("https://google.com".to_string()), - name: Some("Simulation Gov".to_string()), - description: Some("Simulation gov desc".to_string()), - neuron_minimum_dissolve_delay_to_vote_seconds: Some(1), - initial_reward_rate_basis_points: Some(30u64), - final_reward_rate_basis_points: Some(20u64), - reward_rate_transition_duration_seconds: Some(1u64), - max_dissolve_delay_seconds: Some(5u64), - max_neuron_age_seconds_for_age_bonus: Some(1u64), - max_dissolve_delay_bonus_percentage: Some(10u64), - max_age_bonus_percentage: Some(10u64), - initial_voting_period_seconds: Some(86401u64), - wait_for_quiet_deadline_increase_seconds: Some(1u64), - restricted_countries: None, - dapp_canisters: None, - min_participants: Some(1), - min_icp_e8s: None, - max_icp_e8s: None, - min_direct_participation_icp_e8s: Some(15u64), - min_participant_icp_e8s: Some(2000u64), - max_direct_participation_icp_e8s: Some(100_000_000u64), - max_participant_icp_e8s: Some(100_000_000u64), - swap_start_timestamp_seconds: Some(start.duration_since(UNIX_EPOCH).unwrap().as_secs()), - swap_due_timestamp_seconds: Some(start.duration_since(UNIX_EPOCH).unwrap().as_secs() + 300), // year 3000 - hopefully we'll all be gone by then, - neuron_basket_construction_parameters: Some(NeuronBasketConstructionParameters { - count: 2, - dissolve_delay_interval_seconds: 2, - }), - nns_proposal_id: Some(1), - neurons_fund_participation: Some(false), - neurons_fund_participants: None, - token_logo: Some("data:image/png;base64,iVBORw0".to_string()), - neurons_fund_participation_constraints: None, - initial_token_distribution: Some(InitialTokenDistribution::FractionalDeveloperVotingPower( - FractionalDeveloperVotingPower { - airdrop_distribution: Some(AirdropDistribution { - airdrop_neurons: vec![], - }), - developer_distribution: Some(DeveloperDistribution { - developer_neurons: vec![ - NeuronDistribution { - controller: Some( - PrincipalId::from_str(&alice_principal.to_string()).unwrap(), - ), - stake_e8s: 4_400_000, - memo: 0, - dissolve_delay_seconds: 0, - vesting_period_seconds: None, - }, - NeuronDistribution { - controller: Some( - PrincipalId::from_str(&alice_principal.to_string()).unwrap(), - ), - stake_e8s: 100_000, - memo: 1, - dissolve_delay_seconds: 2, - vesting_period_seconds: None, - }, - ], - }), - treasury_distribution: Some(TreasuryDistribution { - total_e8s: 10_000_000, - }), - swap_distribution: Some(SwapDistribution { - total_e8s: 5_000_000, - initial_swap_amount_e8s: 5_000_000, - }), - }, - )), - }; + include_bytes!("../../../../../wasms/index.wasm.gz"), + 6, + ); - let res = pocket_ic - .update_call( - alice_canister_id, - alice_principal, - "deploy_cdao_sns", - candid::encode_args((sns_init_args, 300 as u64)).unwrap(), - ) - .map(|res| { - let response: Result = match res { - WasmResult::Reply(payload) => { - ic_cdk::println!("๐Ÿงช Call made"); - Decode!(&payload, Result).unwrap() - } - _ => panic!("\n๐Ÿ›‘ deploy cdao failed with {:?}", res), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .query_call( - alice_canister_id, - alice_principal, - "get_well_known_principal_value", - candid::encode_one((KnownPrincipalType::CanisterIdSnsWasm)).unwrap(), - ) - .map(|res| { - let response: Option = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get_well_known_principal_value failed"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res.unwrap().to_string()); - - let res = pocket_ic - .query_call( - alice_canister_id, - alice_principal, - "deployed_cdao_canisters", - candid::encode_one(()).unwrap(), - ) - .map(|res| { - let response: Vec = match res { - WasmResult::Reply(payload) => candid::decode_one(&payload).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - for can in &res { - ic_cdk::println!("๐Ÿงช Gov Canister ID: {:?}", can.governance.to_string()); - ic_cdk::println!("๐Ÿงช Ind Canister ID: {:?}", can.index.to_string()); - ic_cdk::println!("๐Ÿงช Ldg Canister ID: {:?}", can.ledger.to_string()); - ic_cdk::println!("๐Ÿงช Rrt Canister ID: {:?}", can.root.to_string()); - ic_cdk::println!("๐Ÿงช Swp Canister ID: {:?}", can.swap.to_string()); - } + for _ in 0..50 { + pic.tick(); + } - assert!(res.len() == 1); - let res = res[0]; - let swap_canister = res.swap; - let gov_canister = res.governance; - let ledger_canister = res.ledger; - ic_cdk::println!("๐Ÿงช๐Ÿงช๐Ÿงช Swap Canister ID: {:?}", swap_canister.to_string()); + Self { + pic, + subnet_orchestrator, + } + } - let res = pocket_ic - .query_call( - Principal::from_text(ICP_LEDGER_CANISTER_ID).unwrap(), - super_admin, - "icrc1_total_supply", - candid::encode_one(()).unwrap(), - ) - .map(|res| { - let response = match res { - WasmResult::Reply(payload) => Decode!(&payload, Nat).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - // check super admin icp balance - let res = pocket_ic - .query_call( - Principal::from_text(ICP_LEDGER_CANISTER_ID).unwrap(), + pub fn provision_individual_canister(&self, owner: Principal, referrer: Option) -> UserCanisterDetails { + let new_canister = execute_update::<_, Result<_, String>>( + &self.pic, + owner, + self.subnet_orchestrator, + "get_requester_principals_canister_id_create_if_not_exists", + &() + ).unwrap(); + let new_canister_details = UserCanisterDetails { + profile_owner: owner, + user_canister_id: new_canister, + }; + + // XX: hack, this is a canister bug, where individual user canister uses mainnet admin id, even in testing + // for certain update calls + let super_admin = Principal::from_str(GLOBAL_SUPER_ADMIN_USER_ID).unwrap(); + let update_res = execute_update::<_, Result>( + &self.pic, super_admin, + new_canister, + "update_session_type", + &SessionType::RegisteredSession + ); + match update_res { + Ok(_) => (), + Err(e) if e == "Session Already marked as Registered Session" => (), + e => panic!("{e:?}"), + }; + + let Some(referrer) = referrer.as_ref() else { + return new_canister_details; + }; + + execute_update::<_, Result>( + &self.pic, + owner, + new_canister, + "update_referrer_details", + &referrer + ).unwrap(); + + execute_update::<_, Result<(), String>>( + &self.pic, + owner, + new_canister, + "receive_reward_for_being_referred", + &() + ).unwrap(); + + // ensure that the referrer also gets the reward + for _ in 0..20 { + self.pic.tick(); + } + + new_canister_details + } + + pub fn icrc1_balance(&self, token_ledger: Principal, acc: Principal) -> Nat { + execute_query( + &self.pic, + acc, + token_ledger, "icrc1_balance_of", - candid::encode_one(types::Icrc1BalanceOfArg { - owner: super_admin, + &types::Icrc1BalanceOfArg { + owner: acc, subaccount: None, - }) - .unwrap(), + } ) - .map(|res| { - let response = match res { - WasmResult::Reply(payload) => Decode!(&payload, Nat).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( - swap_canister, + } + + pub fn create_new_token(&self, user: UserCanisterDetails) -> DeployedCdaoCanisters { + let start = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap(); + let tx_fee = 1u64; + + let super_admin = get_global_super_admin_principal_id(); + let sns_init = SnsInitPayload { + confirmation_text: Some("GET RICH QUICK".into()), + transaction_fee_e8s: Some(tx_fee), + token_name: Some("Simulation Governance".to_string()), + token_symbol: Some("SIMG".to_string()), + proposal_reject_cost_e8s: Some(1u64), + neuron_minimum_stake_e8s: Some(2u64), + fallback_controller_principal_ids: vec![super_admin.to_string()], + logo: Some("data:image/png;base64,iVBORw0".to_string()), + url: Some("https://google.com".to_string()), + name: Some("Simulation Governance".to_string()), + description: Some("Simulation gov desc".to_string()), + neuron_minimum_dissolve_delay_to_vote_seconds: Some(1), + initial_reward_rate_basis_points: Some(30u64), + final_reward_rate_basis_points: Some(20u64), + reward_rate_transition_duration_seconds: Some(1u64), + max_dissolve_delay_seconds: Some(5u64), + max_neuron_age_seconds_for_age_bonus: Some(1u64), + max_dissolve_delay_bonus_percentage: Some(10u64), + max_age_bonus_percentage: Some(10u64), + initial_voting_period_seconds: Some(86401u64), + wait_for_quiet_deadline_increase_seconds: Some(1u64), + restricted_countries: None, + dapp_canisters: None, + min_participants: Some(1), + min_icp_e8s: None, + max_icp_e8s: None, + min_direct_participation_icp_e8s: Some(15u64), + min_participant_icp_e8s: Some(2000u64), + max_direct_participation_icp_e8s: Some(100_000_000u64), + max_participant_icp_e8s: Some(100_000_000u64), + swap_start_timestamp_seconds: Some(start.as_secs()), + swap_due_timestamp_seconds: Some(start.as_secs() + 300), + neuron_basket_construction_parameters: Some(NeuronBasketConstructionParameters { + count: 2, + dissolve_delay_interval_seconds: 2, + }), + nns_proposal_id: Some(1), + neurons_fund_participation: Some(false), + neurons_fund_participants: None, + token_logo: Some("data:image/png;base64,iVBORw0".to_string()), + neurons_fund_participation_constraints: None, + initial_token_distribution: Some(InitialTokenDistribution::FractionalDeveloperVotingPower( + FractionalDeveloperVotingPower { + airdrop_distribution: Some(AirdropDistribution { + airdrop_neurons: vec![], + }), + developer_distribution: Some(DeveloperDistribution { + developer_neurons: vec![ + NeuronDistribution { + controller: Some( + user.profile_owner.into() + ), + stake_e8s: 1_250_001 * (1e8 as u64), + memo: 0, + dissolve_delay_seconds: 0, + vesting_period_seconds: None, + }, + NeuronDistribution { + controller: Some( + user.profile_owner.into() + ), + stake_e8s: 1000 * (1e8 as u64), + memo: 1, + dissolve_delay_seconds: 2, + vesting_period_seconds: None, + }, + ], + }), + treasury_distribution: Some(TreasuryDistribution { + total_e8s: 0, + }), + swap_distribution: Some(SwapDistribution { + total_e8s: 1_251_004 * (1e8 as u64), + initial_swap_amount_e8s: 1_251_004 * (1e8 as u64), + }), + }, + )), + }; + + let deploy_res: DeployedCdaoCanisters = execute_update_multi::<_, Result<_, CdaoDeployError>>( + &self.pic, + user.profile_owner, + user.user_canister_id, + "deploy_cdao_sns", + ( + sns_init, + 300u64 + ) + ).unwrap(); + + let res = execute_update::<_, NewSaleTicketResponse>( + &self.pic, super_admin, + deploy_res.swap, "new_sale_ticket", - candid::encode_one(NewSaleTicketRequest { + &NewSaleTicketRequest { amount_icp_e8s: 1000000, subaccount: None, - }) - .unwrap(), - ) - .map(|res| { - let response: NewSaleTicketResponse = match res { - WasmResult::Reply(payload) => Decode!(&payload, NewSaleTicketResponse).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let subaccount = Subaccount::from(&PrincipalId(super_admin)); - let transfer_args = types::Transaction { - memo: Some(vec![0]), - amount: Nat::from(1000000 as u64), - fee: Some(Nat::from(0 as u64)), - from_subaccount: None, - to: types::Recipient { - owner: swap_canister, - subaccount: Some(subaccount.to_vec()), - }, - created_at_time: None, - }; - let res = pocket_ic - .update_call( - Principal::from_text(ICP_LEDGER_CANISTER_ID).unwrap(), + } + ).result.unwrap(); + assert!(matches!(res, new_sale_ticket_response::Result::Ok(_))); + + let super_adm_sub_acc = Subaccount::from(&PrincipalId(super_admin)); + let transfer_icp_args = types::Transaction { + memo: Some(vec![0]), + amount: Nat::from(1000000u64), + fee: None, + from_subaccount: None, + to: types::Recipient { + owner: deploy_res.swap, + subaccount: Some(super_adm_sub_acc.to_vec()), + }, + created_at_time: None, + }; + + let res = execute_update::<_, types::TransferResult>( + &self.pic, super_admin, + Principal::from_text(ICP_LEDGER_CANISTER_ID).unwrap(), "icrc1_transfer", - Encode!(&transfer_args).unwrap(), - ) - .map(|res| { - let response: types::TransferResult = match res { - WasmResult::Reply(payload) => Decode!(&payload, types::TransferResult).unwrap(), - _ => panic!("\n๐Ÿ›‘ icrc1_transfer failed with: {:?}", res), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( - swap_canister, + &transfer_icp_args, + ); + assert!(matches!(res, types::TransferResult::Ok(_)), "{res:?}"); + + execute_update_no_res( + &self.pic, super_admin, + deploy_res.swap, "refresh_buyer_tokens", - candid::encode_one(RefreshBuyerTokensRequest { + &RefreshBuyerTokensRequest { buyer: super_admin.to_string(), - confirmation_text: Some("GET RICH QUICK".to_string()), - }) - .unwrap(), - ) - .map(|res| { - let response: RefreshBuyerTokensResponse = match res { - WasmResult::Reply(payload) => { - Decode!(&payload, RefreshBuyerTokensResponse).unwrap() - } - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - pocket_ic.advance_time(Duration::from_secs(301)); - for _ in 0..500 { - pocket_ic.tick(); - } + confirmation_text: Some("GET RICH QUICK".to_string()) + } + ); + + // wait for governance to initialize + self.pic.advance_time(Duration::from_secs(301)); + loop { + self.pic.tick(); + let GetModeResponse { mode } = execute_query( + &self.pic, + Principal::anonymous(), + deploy_res.governance, + "get_mode", + &GetMode {} + ); + let mode = mode.unwrap(); + assert!(mode > 0 && mode <= 2); + // mode 1 == MODE_NORMAL + if mode == 1 { + break; + } + } - let res = pocket_ic - .query_call( - swap_canister, - super_admin, - "get_init", - candid::encode_one(GetInitRequest {}).unwrap(), - ) - .map(|res| { - let response = match res { - WasmResult::Reply(payload) => Decode!(&payload, GetInitResponse).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .update_call( - gov_canister, + let neurons: ListNeuronsResponse = execute_query( + &self.pic, super_admin, + deploy_res.governance, "list_neurons", - candid::encode_one(ListNeurons { - of_principal: Some(PrincipalId(alice_principal)), + &ListNeurons { + of_principal: Some(PrincipalId(user.profile_owner)), limit: 2, - start_page_at: None, - }) - .unwrap(), - ) - .map(|res| { - let response: ListNeuronsResponse = match res { - WasmResult::Reply(payload) => Decode!(&payload, ListNeuronsResponse).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let neurons = res.neurons; - let mut ix = 0; - if neurons[1].dissolve_state.is_some() { - if let Some(neuron::DissolveState::DissolveDelaySeconds(x)) = - neurons[1].dissolve_state.as_ref() - { - if *x == 0 { - ix = 1; + start_page_at: None + } + ); + + let neurons = neurons.neurons; + let mut ix = 0; + if neurons[1].dissolve_state.is_some() { + if let Some(neuron::DissolveState::DissolveDelaySeconds(x)) = + neurons[1].dissolve_state.as_ref() + { + if *x == 0 { + ix = 1; + } } } - } - let neuron_id = neurons[ix].id.as_ref().unwrap().id.clone(); - let amount = neurons[ix].cached_neuron_stake_e8s; - let manage_neuron_arg = ManageNeuron { - subaccount: neuron_id, - command: Some(manage_neuron::Command::Disburse(manage_neuron::Disburse { - to_account: Some(Account { - owner: Some(PrincipalId(alice_principal)), - subaccount: None, - }), - amount: Some(manage_neuron::disburse::Amount { e8s: amount }), - })), - }; - let res = pocket_ic - .update_call( - gov_canister, - alice_principal, + let neuron_id = neurons[ix].id.as_ref().unwrap().id.clone(); + let amount = neurons[ix].cached_neuron_stake_e8s; + let manage_neuron = ManageNeuron { + subaccount: neuron_id, + command: Some(manage_neuron::Command::Disburse(manage_neuron::Disburse { + to_account: Some(Account { + owner: Some(PrincipalId(user.profile_owner)), + subaccount: None, + }), + amount: Some(manage_neuron::disburse::Amount { e8s: amount }) + })) + }; + + let res: ManageNeuronResponse = execute_update( + &self.pic, + user.profile_owner, + deploy_res.governance, "manage_neuron", - candid::encode_one(manage_neuron_arg).unwrap(), - ) - .map(|res| { - let response: ManageNeuronResponse = match res { - WasmResult::Reply(payload) => Decode!(&payload, ManageNeuronResponse).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช Result: {:?}", res); - - let res = pocket_ic - .query_call( - ledger_canister, - alice_principal, - "icrc1_balance_of", - candid::encode_one(types::Icrc1BalanceOfArg { - owner: alice_principal, - subaccount: None, - }) - .unwrap(), - ) - .map(|res| { - let response = match res { - WasmResult::Reply(payload) => Decode!(&payload, Nat).unwrap(), - _ => panic!("\n๐Ÿ›‘ get requester principals canister id failed\n"), - }; - response - }) - .unwrap(); - ic_cdk::println!("๐Ÿงช SNS token Balance of alice: {:?}", res); + &manage_neuron, + ); + res.command.unwrap(); + + + let bal = self.icrc1_balance( + deploy_res.ledger, + user.profile_owner + ); + + let expected_balance = tokens_to_e8s(1_250_001) - tx_fee; + assert_eq!(bal, expected_balance); + + // pass 10% of the overall supply to the user's canister + // i.e pass 20% of the user's share as user share is 50% + let canister_share = (tokens_to_e8s(1_250_001) * 20u32) / 100u32; + let res = execute_update::<_, types::TransferResult>( + &self.pic, + user.profile_owner, + deploy_res.ledger, + "icrc1_transfer", + &types::TransferArg { + to: types::Account { + owner: user.user_canister_id, + subaccount: None, + }, + fee: None, + memo: None, + from_subaccount: None, + created_at_time: None, + amount: canister_share, + } + ); + assert!(matches!(res, types::TransferResult::Ok(_))); + + execute_update::<_, Result<(), String>>( + &self.pic, + user.profile_owner, + user.user_canister_id, + "distribute_newly_created_token_to_token_chain", + &deploy_res + ).unwrap(); + + deploy_res + } +} - let expected_balance = Nat::from(4_400_000 - tx_fee); - ic_cdk::println!("๐Ÿงช Expected Balance: {:?}", expected_balance); +#[test] +fn creator_dao_tests() { + let harness = CDaoHarness::init(); - let alice_canister_final_cycle_balance = pocket_ic.cycle_balance(alice_canister_id); + let alice_principal = get_mock_user_alice_principal_id(); + let alice = harness.provision_individual_canister(alice_principal, None); - assert!(alice_canister_final_cycle_balance > alice_initial_cycle_balance); - assert!(res == expected_balance); + let sns_wasm_w_canister_id: Option = execute_query( + &harness.pic, + alice_principal, + alice.user_canister_id, + "get_well_known_principal_value", + &KnownPrincipalType::CanisterIdSnsWasm, + ); + let res: HashMap = execute_query( + &harness.pic, + Principal::anonymous(), + sns_wasm_w_canister_id.unwrap(), + "get_latest_sns_version_pretty", + &() + ); + assert_eq!(res.len(), 6); + + harness.create_new_token(alice); } + diff --git a/src/lib/integration_tests/tests/hot_or_not/bet_maker_canister_tests.rs b/src/lib/integration_tests/tests/hot_or_not/bet_maker_canister_tests.rs index 9337392d..f592950c 100644 --- a/src/lib/integration_tests/tests/hot_or_not/bet_maker_canister_tests.rs +++ b/src/lib/integration_tests/tests/hot_or_not/bet_maker_canister_tests.rs @@ -456,6 +456,7 @@ fn when_bet_maker_places_bet_on_a_post_it_is_assigned_a_slot_id_and_the_outcome_ known_principal_ids: None, profile_owner: None, url_to_send_canister_metrics_to: None, + proof_of_participation: None, }; pocket_ic diff --git a/src/lib/integration_tests/tests/hot_or_not/hot_or_not_timely_index_update_test.rs b/src/lib/integration_tests/tests/hot_or_not/hot_or_not_timely_index_update_test.rs index ab24672d..38f50e6a 100644 --- a/src/lib/integration_tests/tests/hot_or_not/hot_or_not_timely_index_update_test.rs +++ b/src/lib/integration_tests/tests/hot_or_not/hot_or_not_timely_index_update_test.rs @@ -79,6 +79,7 @@ fn hot_or_not_timely_update_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); diff --git a/src/lib/integration_tests/tests/hot_or_not/hotornot_game_simulation_test.rs b/src/lib/integration_tests/tests/hot_or_not/hotornot_game_simulation_test.rs index 0f92616a..c166a44a 100644 --- a/src/lib/integration_tests/tests/hot_or_not/hotornot_game_simulation_test.rs +++ b/src/lib/integration_tests/tests/hot_or_not/hotornot_game_simulation_test.rs @@ -90,6 +90,7 @@ fn hotornot_game_simulation_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -111,6 +112,7 @@ fn hotornot_game_simulation_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -132,6 +134,7 @@ fn hotornot_game_simulation_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -153,6 +156,7 @@ fn hotornot_game_simulation_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); diff --git a/src/lib/integration_tests/tests/hot_or_not/reconcile_scores_test.rs b/src/lib/integration_tests/tests/hot_or_not/reconcile_scores_test.rs index 3544dd64..98005334 100644 --- a/src/lib/integration_tests/tests/hot_or_not/reconcile_scores_test.rs +++ b/src/lib/integration_tests/tests/hot_or_not/reconcile_scores_test.rs @@ -91,6 +91,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -112,6 +113,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -133,6 +135,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -154,6 +157,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -963,6 +967,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -984,6 +989,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -1005,6 +1011,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); @@ -1026,6 +1033,7 @@ fn reconcile_scores_test() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); diff --git a/src/lib/integration_tests/tests/post/update_post_status_banned.rs b/src/lib/integration_tests/tests/post/update_post_status_banned.rs index b5a55b59..244f1b85 100644 --- a/src/lib/integration_tests/tests/post/update_post_status_banned.rs +++ b/src/lib/integration_tests/tests/post/update_post_status_banned.rs @@ -86,6 +86,7 @@ fn update_post_status_banned() { upgrade_version_number: None, url_to_send_canister_metrics_to: None, version: "1".to_string(), + proof_of_participation: None, }; let individual_template_args_bytes = encode_one(individual_template_args).unwrap(); diff --git a/src/lib/shared_utils/Cargo.toml b/src/lib/shared_utils/Cargo.toml index 80430800..89bb3ea9 100644 --- a/src/lib/shared_utils/Cargo.toml +++ b/src/lib/shared_utils/Cargo.toml @@ -17,6 +17,16 @@ ciborium = { workspace = true } serde_json_any_key = "2.0.0" serde_bytes = "0.11.14" icrc-ledger-types = { workspace = true } +ed25519-compact = { version = "2.1.1", default-features = false, features = ["std"] } +trie-db = { version = "0.29.1", default-features = false } +hash-db = { version = "0.16.0", default-features = false } +blake3 = "1.5.4" +hash256-std-hasher = { version = "0.15.2", default-features = false } +parity-scale-codec = { version = "3.6.12", default-features = false } +memory-db = { version = "0.32.0", default-features = false } +trie-root = { version = "0.18.0", default-features = false } +# monotree = { git = "https://github.com/rupansh-sekar-yral/monotree.git", rev = "1504007dae91a909fbb29bfebb36181ef3333cb8" } +# hashbrown = { version = "0.7", features = ["serde"] } [dev-dependencies] test_utils = { workspace = true } diff --git a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/airdrop.rs b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/airdrop.rs new file mode 100644 index 00000000..eb9c2888 --- /dev/null +++ b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/airdrop.rs @@ -0,0 +1,28 @@ +use std::borrow::Cow; + +use candid::{CandidType, Principal}; +use ic_stable_structures::{storable::Bound, Storable}; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AirdropMember { + pub user_principal: Principal, + pub user_canister: Principal, +} + +impl Storable for AirdropMember { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut bytes = Vec::new(); + ciborium::into_writer(self, &mut bytes) + .expect("Expected to serialize AirdropMember"); + + bytes.into() + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + ciborium::from_reader(bytes.as_ref()) + .expect("Expected to deserialize AirdropMember") + } +} diff --git a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/arg.rs b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/arg.rs index dd9f7aab..d5a69700 100644 --- a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/arg.rs +++ b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/arg.rs @@ -1,6 +1,6 @@ use candid::{CandidType, Deserialize, Principal}; -use crate::common::types::known_principal::KnownPrincipalMap; +use crate::common::{participant_crypto::ProofOfParticipation, types::known_principal::KnownPrincipalMap}; use super::hot_or_not::BetDirection; @@ -11,6 +11,7 @@ pub struct IndividualUserTemplateInitArgs { pub upgrade_version_number: Option, pub url_to_send_canister_metrics_to: Option, pub version: String, + pub proof_of_participation: Option, } #[derive(Deserialize, CandidType, Clone)] diff --git a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/ml_data/mod.rs b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/ml_data/mod.rs index 5e98ad2f..6b0ae1ef 100644 --- a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/ml_data/mod.rs +++ b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/ml_data/mod.rs @@ -10,7 +10,7 @@ use ic_stable_structures::storable::Bound; use ic_stable_structures::Storable; use serde::Serialize; -#[derive(Deserialize, Serialize, PartialEq, PartialOrd, Clone, CandidType, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Clone, CandidType, Debug)] pub struct WatchHistoryItem { pub post_id: u64, pub publisher_canister_id: Principal, @@ -19,6 +19,12 @@ pub struct WatchHistoryItem { pub percentage_watched: f32, } +impl PartialOrd for WatchHistoryItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Ord for WatchHistoryItem { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.viewed_at.cmp(&other.viewed_at) @@ -43,7 +49,7 @@ impl Storable for WatchHistoryItem { } #[deprecated(note = "use SuccessHistoryItemV1 instead")] -#[derive(Deserialize, Serialize, PartialEq, PartialOrd, Clone, CandidType, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Clone, CandidType, Debug)] pub struct SuccessHistoryItem { pub post_id: u64, pub publisher_canister_id: Principal, @@ -51,6 +57,12 @@ pub struct SuccessHistoryItem { pub cf_video_id: String, } +impl PartialOrd for SuccessHistoryItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Ord for SuccessHistoryItem { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.interacted_at.cmp(&other.interacted_at) @@ -74,7 +86,7 @@ impl Storable for SuccessHistoryItem { }; } -#[derive(Deserialize, Serialize, PartialEq, PartialOrd, Clone, CandidType, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Clone, CandidType, Debug)] pub struct SuccessHistoryItemV1 { pub post_id: u64, pub publisher_canister_id: Principal, @@ -84,6 +96,12 @@ pub struct SuccessHistoryItemV1 { pub percentage_watched: f32, } +impl PartialOrd for SuccessHistoryItemV1 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Ord for SuccessHistoryItemV1 { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.interacted_at.cmp(&other.interacted_at) diff --git a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/mod.rs b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/mod.rs index 1ff67c63..f29359d9 100644 --- a/src/lib/shared_utils/src/canister_specific/individual_user_template/types/mod.rs +++ b/src/lib/shared_utils/src/canister_specific/individual_user_template/types/mod.rs @@ -12,3 +12,4 @@ pub mod session; pub mod token; pub mod cdao; pub mod device_id; +pub mod airdrop; diff --git a/src/lib/shared_utils/src/canister_specific/user_index/types/args.rs b/src/lib/shared_utils/src/canister_specific/user_index/types/args.rs index 3a6182ea..12211696 100644 --- a/src/lib/shared_utils/src/canister_specific/user_index/types/args.rs +++ b/src/lib/shared_utils/src/canister_specific/user_index/types/args.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use candid::{CandidType, Deserialize, Principal}; -use crate::{access_control::UserAccessRole, common::types::known_principal::KnownPrincipalMap}; +use crate::{access_control::UserAccessRole, common::{participant_crypto::ProofOfParticipation, types::known_principal::KnownPrincipalMap}}; #[derive(Deserialize, CandidType, Default, Clone)] pub struct UserIndexInitArgs { pub known_principal_ids: Option, pub access_control_map: Option>>, - pub version: String + pub version: String, + pub proof_of_participation: Option, } diff --git a/src/lib/shared_utils/src/common/mod.rs b/src/lib/shared_utils/src/common/mod.rs index 5b907739..93a25135 100644 --- a/src/lib/shared_utils/src/common/mod.rs +++ b/src/lib/shared_utils/src/common/mod.rs @@ -2,3 +2,4 @@ pub mod environment; pub mod timer; pub mod types; pub mod utils; +pub mod participant_crypto; diff --git a/src/lib/shared_utils/src/common/participant_crypto/merkle/backing.rs b/src/lib/shared_utils/src/common/participant_crypto/merkle/backing.rs new file mode 100644 index 00000000..1b087bf4 --- /dev/null +++ b/src/lib/shared_utils/src/common/participant_crypto/merkle/backing.rs @@ -0,0 +1,113 @@ +use std::collections::{btree_map::Entry, BTreeMap}; + +use hash_db::{AsHashDB, HashDB, HashDBRef, Hasher, Prefix}; +use memory_db::{HashKey, KeyFunction}; +use serde::{Deserialize, Serialize}; +use trie_db::{TrieLayout, NodeCodec}; + +use super::{Blake3Hasher, ChildreenTreeLayout, Hash}; + +type ChildrenHK = HashKey; + +// https://docs.rs/memory-db/0.32.0/src/memory_db/lib.rs.html#83-92 modified for our uses +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChildrenBacking { + data: BTreeMap, i32)>, + hashed_null_node: Hash, + null_node_data: Vec, +} + +impl Default for ChildrenBacking { + fn default() -> Self { + Self { + data: BTreeMap::new(), + hashed_null_node: ::Codec::hashed_null_node(), + null_node_data: ::Codec::empty_node().to_vec(), + } + } +} + +impl HashDB> for ChildrenBacking { + fn get(&self, key: &Hash, prefix: Prefix) -> Option> { + if key == &self.hashed_null_node { + return Some(self.null_node_data.clone()); + } + + let key = ChildrenHK::key(key, prefix); + match self.data.get(&key) { + Some(&(ref v, rc)) if rc > 0 => Some(v.clone()), + _ => None, + } + } + + fn contains(&self, key: &Hash, prefix: Prefix) -> bool { + if key == &self.hashed_null_node { + return true; + } + + let key = ChildrenHK::key(key, prefix); + self.data.get(&key).map(|&(_, rc)| rc > 0).unwrap_or_default() + } + + fn emplace(&mut self, key: Hash, prefix: Prefix, value: Vec) { + if value == self.null_node_data { + return; + } + + let key = ChildrenHK::key(&key, prefix); + match self.data.entry(key) { + Entry::Occupied(mut entry) => { + let &mut (ref mut old_value, ref mut rc) = entry.get_mut(); + if *rc <= 0 { + *old_value = value; + } + *rc += 1; + }, + Entry::Vacant(entry) => { + entry.insert((value, 1)); + } + } + } + + fn insert(&mut self, prefix: Prefix, value: &[u8]) -> Hash { + if value == self.null_node_data { + return self.hashed_null_node; + } + + let key = Blake3Hasher::hash(value); + self.emplace(key, prefix, value.to_vec()); + key + } + + fn remove(&mut self, key: &Hash, prefix: Prefix) { + if key == &self.hashed_null_node { + return; + } + + let key = ChildrenHK::key(key, prefix); + self.data.entry(key) + .and_modify(|&mut (_, ref mut rc)| *rc -= 1) + .or_insert_with(|| (vec![], -1)); + } +} + +impl AsHashDB> for ChildrenBacking { + fn as_hash_db(&self) -> &dyn HashDB> { + self + } + + fn as_hash_db_mut<'a>(&'a mut self) -> &'a mut (dyn HashDB> + 'a) { + self + } +} + +impl HashDBRef> for ChildrenBacking { + fn get(&self, key: &Hash, prefix: Prefix) -> Option> { + HashDB::get(self, key, prefix) + } + + fn contains(&self, key: &Hash, prefix: Prefix) -> bool { + HashDB::contains(self, key, prefix) + } +} + diff --git a/src/lib/shared_utils/src/common/participant_crypto/merkle/layout.rs b/src/lib/shared_utils/src/common/participant_crypto/merkle/layout.rs new file mode 100644 index 00000000..41734d0e --- /dev/null +++ b/src/lib/shared_utils/src/common/participant_crypto/merkle/layout.rs @@ -0,0 +1,708 @@ +// Taken directly from https://github.com/paritytech/trie/blob/master/test-support/reference-trie/src/substrate.rs + +use core::{borrow::Borrow, iter::once, marker::PhantomData, ops::Range}; +use hash_db::Hasher; +use parity_scale_codec as codec; +use parity_scale_codec::{Compact, Decode, Encode, Input, Output}; +use trie_db::{ + nibble_ops, + node::{NibbleSlicePlan, NodeHandlePlan, NodePlan, Value, ValuePlan}, + ChildReference, NodeCodec as NodeCodecT, TrieConfiguration, TrieLayout, +}; + +/// Constants used into trie simplification codec. +mod trie_constants { + const FIRST_PREFIX: u8 = 0b_00 << 6; + pub const LEAF_PREFIX_MASK: u8 = 0b_01 << 6; + pub const BRANCH_WITHOUT_MASK: u8 = 0b_10 << 6; + pub const BRANCH_WITH_MASK: u8 = 0b_11 << 6; + pub const EMPTY_TRIE: u8 = FIRST_PREFIX /*| (0b_00 << 4)*/; + pub const ALT_HASHING_LEAF_PREFIX_MASK: u8 = FIRST_PREFIX | (0b_1 << 5); + pub const ALT_HASHING_BRANCH_WITH_MASK: u8 = FIRST_PREFIX | (0b_01 << 4); + pub const ESCAPE_COMPACT_HEADER: u8 = EMPTY_TRIE | 0b_00_01; +} + +pub const TRIE_VALUE_NODE_THRESHOLD: u32 = 33; + +/// Codec-flavored TrieStream. +#[derive(Default, Clone)] +pub struct TrieStream { + /// Current node buffer. + buffer: Vec, +} + +fn branch_node_bit_mask(has_children: impl Iterator) -> (u8, u8) { + let mut bitmap: u16 = 0; + let mut cursor: u16 = 1; + for v in has_children { + if v { + bitmap |= cursor + } + cursor <<= 1; + } + ((bitmap % 256) as u8, (bitmap / 256) as u8) +} + +/// Create a leaf/branch node, encoding a number of nibbles. +fn fuse_nibbles_node(nibbles: &[u8], kind: NodeKind) -> impl Iterator + '_ { + let size = nibbles.len(); + let iter_start = match kind { + NodeKind::Leaf => size_and_prefix_iterator(size, trie_constants::LEAF_PREFIX_MASK, 2), + NodeKind::BranchNoValue => + size_and_prefix_iterator(size, trie_constants::BRANCH_WITHOUT_MASK, 2), + NodeKind::BranchWithValue => + size_and_prefix_iterator(size, trie_constants::BRANCH_WITH_MASK, 2), + NodeKind::HashedValueLeaf => + size_and_prefix_iterator(size, trie_constants::ALT_HASHING_LEAF_PREFIX_MASK, 3), + NodeKind::HashedValueBranch => + size_and_prefix_iterator(size, trie_constants::ALT_HASHING_BRANCH_WITH_MASK, 4), + }; + iter_start + .chain(if nibbles.len() % 2 == 1 { Some(nibbles[0]) } else { None }) + .chain(nibbles[nibbles.len() % 2..].chunks(2).map(|ch| ch[0] << 4 | ch[1])) +} + +use trie_root::Value as TrieStreamValue; +impl trie_root::TrieStream for TrieStream { + fn new() -> Self { + Self { buffer: Vec::new() } + } + + fn append_empty_data(&mut self) { + self.buffer.push(trie_constants::EMPTY_TRIE); + } + + fn append_leaf(&mut self, key: &[u8], value: TrieStreamValue) { + let kind = match &value { + TrieStreamValue::Inline(..) => NodeKind::Leaf, + TrieStreamValue::Node(..) => NodeKind::HashedValueLeaf, + }; + self.buffer.extend(fuse_nibbles_node(key, kind)); + match &value { + TrieStreamValue::Inline(value) => { + Compact(value.len() as u32).encode_to(&mut self.buffer); + self.buffer.extend_from_slice(value); + }, + TrieStreamValue::Node(hash) => { + self.buffer.extend_from_slice(hash.as_slice()); + }, + }; + } + + fn begin_branch( + &mut self, + maybe_partial: Option<&[u8]>, + maybe_value: Option, + has_children: impl Iterator, + ) { + if let Some(partial) = maybe_partial { + let kind = match &maybe_value { + None => NodeKind::BranchNoValue, + Some(TrieStreamValue::Inline(..)) => NodeKind::BranchWithValue, + Some(TrieStreamValue::Node(..)) => NodeKind::HashedValueBranch, + }; + + self.buffer.extend(fuse_nibbles_node(partial, kind)); + let bm = branch_node_bit_mask(has_children); + self.buffer.extend([bm.0, bm.1].iter()); + } else { + unreachable!("trie stream codec only for no extension trie"); + } + match maybe_value { + None => (), + Some(TrieStreamValue::Inline(value)) => { + Compact(value.len() as u32).encode_to(&mut self.buffer); + self.buffer.extend_from_slice(value); + }, + Some(TrieStreamValue::Node(hash)) => { + self.buffer.extend_from_slice(hash.as_slice()); + }, + } + } + + fn append_extension(&mut self, _key: &[u8]) { + debug_assert!(false, "trie stream codec only for no extension trie"); + } + + fn append_substream(&mut self, other: Self) { + let data = other.out(); + match data.len() { + 0..=31 => data.encode_to(&mut self.buffer), + _ => H::hash(&data).as_ref().encode_to(&mut self.buffer), + } + } + + fn out(self) -> Vec { + self.buffer + } +} + +/// Helper struct for trie node decoder. This implements `codec::Input` on a byte slice, while +/// tracking the absolute position. This is similar to `std::io::Cursor` but does not implement +/// `Read` and `io` is not in `sp-std`. +struct ByteSliceInput<'a> { + data: &'a [u8], + offset: usize, +} + +impl<'a> ByteSliceInput<'a> { + fn new(data: &'a [u8]) -> Self { + ByteSliceInput { data, offset: 0 } + } + + fn take(&mut self, count: usize) -> Result, codec::Error> { + if self.offset + count > self.data.len() { + return Err("out of data".into()) + } + + let range = self.offset..(self.offset + count); + self.offset += count; + Ok(range) + } +} + +impl<'a> Input for ByteSliceInput<'a> { + fn remaining_len(&mut self) -> Result, codec::Error> { + Ok(Some(self.data.len().saturating_sub(self.offset))) + } + + fn read(&mut self, into: &mut [u8]) -> Result<(), codec::Error> { + let range = self.take(into.len())?; + into.copy_from_slice(&self.data[range]); + Ok(()) + } + + fn read_byte(&mut self) -> Result { + if self.offset + 1 > self.data.len() { + return Err("out of data".into()) + } + + let byte = self.data[self.offset]; + self.offset += 1; + Ok(byte) + } +} + +/// Concrete implementation of a [`NodeCodecT`] with SCALE encoding. +/// +/// It is generic over `H` the [`Hasher`]. +#[derive(Default, Clone)] +pub struct NodeCodec(PhantomData); + +impl NodeCodecT for NodeCodec +where + H: Hasher, +{ + const ESCAPE_HEADER: Option = Some(trie_constants::ESCAPE_COMPACT_HEADER); + type Error = Error; + type HashOut = H::Out; + + fn hashed_null_node() -> ::Out { + H::hash(::empty_node()) + } + + fn decode_plan(data: &[u8]) -> Result { + let mut input = ByteSliceInput::new(data); + + let header = NodeHeader::decode(&mut input)?; + let contains_hash = header.contains_hash_of_value(); + + let branch_has_value = if let NodeHeader::Branch(has_value, _) = &header { + *has_value + } else { + // hashed_value_branch + true + }; + + match header { + NodeHeader::Null => Ok(NodePlan::Empty), + NodeHeader::HashedValueBranch(nibble_count) | NodeHeader::Branch(_, nibble_count) => { + let padding = nibble_count % nibble_ops::NIBBLE_PER_BYTE != 0; + // check that the padding is valid (if any) + if padding && nibble_ops::pad_left(data[input.offset]) != 0 { + return Err(Error::BadFormat) + } + let partial = input.take( + (nibble_count + (nibble_ops::NIBBLE_PER_BYTE - 1)) / + nibble_ops::NIBBLE_PER_BYTE, + )?; + let partial_padding = nibble_ops::number_padding(nibble_count); + let bitmap_range = input.take(BITMAP_LENGTH)?; + let bitmap = Bitmap::decode(&data[bitmap_range])?; + let value = if branch_has_value { + Some(if contains_hash { + ValuePlan::Node(input.take(H::LENGTH)?) + } else { + let count = >::decode(&mut input)?.0 as usize; + ValuePlan::Inline(input.take(count)?) + }) + } else { + None + }; + let mut children = [ + None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, + ]; + for (i, child) in children.iter_mut().enumerate().take(nibble_ops::NIBBLE_LENGTH) { + if bitmap.value_at(i) { + let count = >::decode(&mut input)?.0 as usize; + let range = input.take(count)?; + *child = Some(if count == H::LENGTH { + NodeHandlePlan::Hash(range) + } else { + NodeHandlePlan::Inline(range) + }); + } + } + Ok(NodePlan::NibbledBranch { + partial: NibbleSlicePlan::new(partial, partial_padding), + value, + children, + }) + }, + NodeHeader::HashedValueLeaf(nibble_count) | NodeHeader::Leaf(nibble_count) => { + let padding = nibble_count % nibble_ops::NIBBLE_PER_BYTE != 0; + // check that the padding is valid (if any) + if padding && nibble_ops::pad_left(data[input.offset]) != 0 { + return Err(Error::BadFormat) + } + let partial = input.take( + (nibble_count + (nibble_ops::NIBBLE_PER_BYTE - 1)) / + nibble_ops::NIBBLE_PER_BYTE, + )?; + let partial_padding = nibble_ops::number_padding(nibble_count); + let value = if contains_hash { + ValuePlan::Node(input.take(H::LENGTH)?) + } else { + let count = >::decode(&mut input)?.0 as usize; + ValuePlan::Inline(input.take(count)?) + }; + + Ok(NodePlan::Leaf { + partial: NibbleSlicePlan::new(partial, partial_padding), + value, + }) + }, + } + } + + fn is_empty_node(data: &[u8]) -> bool { + data == ::empty_node() + } + + fn empty_node() -> &'static [u8] { + &[trie_constants::EMPTY_TRIE] + } + + fn leaf_node(partial: impl Iterator, number_nibble: usize, value: Value) -> Vec { + let contains_hash = matches!(&value, Value::Node(..)); + let mut output = if contains_hash { + partial_from_iterator_encode(partial, number_nibble, NodeKind::HashedValueLeaf) + } else { + partial_from_iterator_encode(partial, number_nibble, NodeKind::Leaf) + }; + match value { + Value::Inline(value) => { + Compact(value.len() as u32).encode_to(&mut output); + output.extend_from_slice(value); + }, + Value::Node(hash) => { + debug_assert!(hash.len() == H::LENGTH); + output.extend_from_slice(hash); + }, + } + output + } + + fn extension_node( + _partial: impl Iterator, + _nbnibble: usize, + _child: ChildReference<::Out>, + ) -> Vec { + unreachable!("No extension codec.") + } + + fn branch_node( + _children: impl Iterator::Out>>>>, + _maybe_value: Option, + ) -> Vec { + unreachable!("No extension codec.") + } + + fn branch_node_nibbled( + partial: impl Iterator, + number_nibble: usize, + children: impl Iterator::Out>>>>, + value: Option, + ) -> Vec { + let contains_hash = matches!(&value, Some(Value::Node(..))); + let mut output = match (&value, contains_hash) { + (&None, _) => + partial_from_iterator_encode(partial, number_nibble, NodeKind::BranchNoValue), + (_, false) => + partial_from_iterator_encode(partial, number_nibble, NodeKind::BranchWithValue), + (_, true) => + partial_from_iterator_encode(partial, number_nibble, NodeKind::HashedValueBranch), + }; + + let bitmap_index = output.len(); + let mut bitmap: [u8; BITMAP_LENGTH] = [0; BITMAP_LENGTH]; + (0..BITMAP_LENGTH).for_each(|_| output.push(0)); + match value { + Some(Value::Inline(value)) => { + Compact(value.len() as u32).encode_to(&mut output); + output.extend_from_slice(value); + }, + Some(Value::Node(hash)) => { + debug_assert!(hash.len() == H::LENGTH); + output.extend_from_slice(hash); + }, + None => (), + } + Bitmap::encode( + children.map(|maybe_child| match maybe_child.borrow() { + Some(ChildReference::Hash(h)) => { + h.as_ref().encode_to(&mut output); + true + }, + &Some(ChildReference::Inline(inline_data, len)) => { + inline_data.as_ref()[..len].encode_to(&mut output); + true + }, + None => false, + }), + bitmap.as_mut(), + ); + output[bitmap_index..bitmap_index + BITMAP_LENGTH] + .copy_from_slice(&bitmap[..BITMAP_LENGTH]); + output + } +} + +// utils + +/// Encode and allocate node type header (type and size), and partial value. +/// It uses an iterator over encoded partial bytes as input. +fn partial_from_iterator_encode>( + partial: I, + nibble_count: usize, + node_kind: NodeKind, +) -> Vec { + let mut output = Vec::with_capacity(4 + (nibble_count / nibble_ops::NIBBLE_PER_BYTE)); + match node_kind { + NodeKind::Leaf => NodeHeader::Leaf(nibble_count).encode_to(&mut output), + NodeKind::BranchWithValue => NodeHeader::Branch(true, nibble_count).encode_to(&mut output), + NodeKind::BranchNoValue => NodeHeader::Branch(false, nibble_count).encode_to(&mut output), + NodeKind::HashedValueLeaf => + NodeHeader::HashedValueLeaf(nibble_count).encode_to(&mut output), + NodeKind::HashedValueBranch => + NodeHeader::HashedValueBranch(nibble_count).encode_to(&mut output), + }; + output.extend(partial); + output +} + +const BITMAP_LENGTH: usize = 2; + +/// Radix 16 trie, bitmap encoding implementation, +/// it contains children mapping information for a branch +/// (children presence only), it encodes into +/// a compact bitmap encoding representation. +pub(crate) struct Bitmap(u16); + +impl Bitmap { + pub fn decode(data: &[u8]) -> Result { + let value = u16::decode(&mut &data[..])?; + if value == 0 { + Err("Bitmap without a child.".into()) + } else { + Ok(Bitmap(value)) + } + } + + pub fn value_at(&self, i: usize) -> bool { + self.0 & (1u16 << i) != 0 + } + + pub fn encode>(has_children: I, dest: &mut [u8]) { + let mut bitmap: u16 = 0; + let mut cursor: u16 = 1; + for v in has_children { + if v { + bitmap |= cursor + } + cursor <<= 1; + } + dest[0] = (bitmap % 256) as u8; + dest[1] = (bitmap / 256) as u8; + } +} + +/// substrate trie layout +pub struct LayoutV0(PhantomData); + +/// substrate trie layout, with external value nodes. +pub struct LayoutV1(PhantomData); + +impl TrieLayout for LayoutV0 +where + H: Hasher + core::fmt::Debug, +{ + const USE_EXTENSION: bool = false; + const ALLOW_EMPTY: bool = true; + const MAX_INLINE_VALUE: Option = None; + + type Hash = H; + type Codec = NodeCodec; +} + +impl TrieConfiguration for LayoutV0 +where + H: Hasher + core::fmt::Debug, +{ + fn trie_root(input: I) -> ::Out + where + I: IntoIterator, + A: AsRef<[u8]> + Ord, + B: AsRef<[u8]>, + { + trie_root::trie_root_no_extension::(input, Self::MAX_INLINE_VALUE) + } + + fn trie_root_unhashed(input: I) -> Vec + where + I: IntoIterator, + A: AsRef<[u8]> + Ord, + B: AsRef<[u8]>, + { + trie_root::unhashed_trie_no_extension::( + input, + Self::MAX_INLINE_VALUE, + ) + } + + fn encode_index(input: u32) -> Vec { + codec::Encode::encode(&codec::Compact(input)) + } +} + +impl TrieLayout for LayoutV1 +where + H: Hasher + core::fmt::Debug, +{ + const USE_EXTENSION: bool = false; + const ALLOW_EMPTY: bool = true; + const MAX_INLINE_VALUE: Option = Some(TRIE_VALUE_NODE_THRESHOLD); + + type Hash = H; + type Codec = NodeCodec; +} + +impl TrieConfiguration for LayoutV1 +where + H: Hasher + core::fmt::Debug, +{ + fn trie_root(input: I) -> ::Out + where + I: IntoIterator, + A: AsRef<[u8]> + Ord, + B: AsRef<[u8]>, + { + trie_root::trie_root_no_extension::(input, Self::MAX_INLINE_VALUE) + } + + fn trie_root_unhashed(input: I) -> Vec + where + I: IntoIterator, + A: AsRef<[u8]> + Ord, + B: AsRef<[u8]>, + { + trie_root::unhashed_trie_no_extension::( + input, + Self::MAX_INLINE_VALUE, + ) + } + + fn encode_index(input: u32) -> Vec { + codec::Encode::encode(&codec::Compact(input)) + } +} + +/// A node header +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum NodeHeader { + Null, + // contains wether there is a value and nibble count + Branch(bool, usize), + // contains nibble count + Leaf(usize), + // contains nibble count. + HashedValueBranch(usize), + // contains nibble count. + HashedValueLeaf(usize), +} + +impl NodeHeader { + pub(crate) fn contains_hash_of_value(&self) -> bool { + matches!(self, NodeHeader::HashedValueBranch(_) | NodeHeader::HashedValueLeaf(_)) + } +} + +/// NodeHeader without content +pub(crate) enum NodeKind { + Leaf, + BranchNoValue, + BranchWithValue, + HashedValueLeaf, + HashedValueBranch, +} + +impl Encode for NodeHeader { + fn encode_to(&self, output: &mut T) { + match self { + NodeHeader::Null => output.push_byte(trie_constants::EMPTY_TRIE), + NodeHeader::Branch(true, nibble_count) => + encode_size_and_prefix(*nibble_count, trie_constants::BRANCH_WITH_MASK, 2, output), + NodeHeader::Branch(false, nibble_count) => encode_size_and_prefix( + *nibble_count, + trie_constants::BRANCH_WITHOUT_MASK, + 2, + output, + ), + NodeHeader::Leaf(nibble_count) => + encode_size_and_prefix(*nibble_count, trie_constants::LEAF_PREFIX_MASK, 2, output), + NodeHeader::HashedValueBranch(nibble_count) => encode_size_and_prefix( + *nibble_count, + trie_constants::ALT_HASHING_BRANCH_WITH_MASK, + 4, + output, + ), + NodeHeader::HashedValueLeaf(nibble_count) => encode_size_and_prefix( + *nibble_count, + trie_constants::ALT_HASHING_LEAF_PREFIX_MASK, + 3, + output, + ), + } + } +} + +impl codec::EncodeLike for NodeHeader {} + +impl Decode for NodeHeader { + fn decode(input: &mut I) -> Result { + let i = input.read_byte()?; + if i == trie_constants::EMPTY_TRIE { + return Ok(NodeHeader::Null) + } + match i & (0b11 << 6) { + trie_constants::LEAF_PREFIX_MASK => Ok(NodeHeader::Leaf(decode_size(i, input, 2)?)), + trie_constants::BRANCH_WITH_MASK => + Ok(NodeHeader::Branch(true, decode_size(i, input, 2)?)), + trie_constants::BRANCH_WITHOUT_MASK => + Ok(NodeHeader::Branch(false, decode_size(i, input, 2)?)), + trie_constants::EMPTY_TRIE => { + if i & (0b111 << 5) == trie_constants::ALT_HASHING_LEAF_PREFIX_MASK { + Ok(NodeHeader::HashedValueLeaf(decode_size(i, input, 3)?)) + } else if i & (0b1111 << 4) == trie_constants::ALT_HASHING_BRANCH_WITH_MASK { + Ok(NodeHeader::HashedValueBranch(decode_size(i, input, 4)?)) + } else { + // do not allow any special encoding + Err("Unallowed encoding".into()) + } + }, + _ => unreachable!(), + } + } +} + +/// Returns an iterator over encoded bytes for node header and size. +/// Size encoding allows unlimited, length inefficient, representation, but +/// is bounded to 16 bit maximum value to avoid possible DOS. +pub(crate) fn size_and_prefix_iterator( + size: usize, + prefix: u8, + prefix_mask: usize, +) -> impl Iterator { + let max_value = 255u8 >> prefix_mask; + let l1 = core::cmp::min((max_value as usize).saturating_sub(1), size); + let (first_byte, mut rem) = if size == l1 { + (once(prefix + l1 as u8), 0) + } else { + (once(prefix + max_value), size - l1) + }; + let next_bytes = move || { + if rem > 0 { + if rem < 256 { + let result = rem - 1; + rem = 0; + Some(result as u8) + } else { + rem = rem.saturating_sub(255); + Some(255) + } + } else { + None + } + }; + first_byte.chain(core::iter::from_fn(next_bytes)) +} + +/// Encodes size and prefix to a stream output. +fn encode_size_and_prefix(size: usize, prefix: u8, prefix_mask: usize, out: &mut W) +where + W: Output + ?Sized, +{ + for b in size_and_prefix_iterator(size, prefix, prefix_mask) { + out.push_byte(b) + } +} + +/// Decode size only from stream input and header byte. +fn decode_size( + first: u8, + input: &mut impl Input, + prefix_mask: usize, +) -> Result { + let max_value = 255u8 >> prefix_mask; + let mut result = (first & max_value) as usize; + if result < max_value as usize { + return Ok(result) + } + result -= 1; + loop { + let n = input.read_byte()? as usize; + if n < 255 { + return Ok(result + n + 1) + } + result += 255; + } +} + +/// Error type used for trie related errors. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Error { + BadFormat, + Decode(codec::Error), + Trie(Box>), +} + +impl core::fmt::Display for Error { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { + fmt.write_str("Error") + } +} + +impl std::error::Error for Error where H: core::fmt::Debug {} + +impl From for Error { + fn from(x: codec::Error) -> Self { + Error::Decode(x) + } +} + +impl From>> for Error { + fn from(x: Box>) -> Self { + Error::Trie(x) + } +} \ No newline at end of file diff --git a/src/lib/shared_utils/src/common/participant_crypto/merkle/mod.rs b/src/lib/shared_utils/src/common/participant_crypto/merkle/mod.rs new file mode 100644 index 00000000..87412a15 --- /dev/null +++ b/src/lib/shared_utils/src/common/participant_crypto/merkle/mod.rs @@ -0,0 +1,103 @@ + +use backing::ChildrenBacking; +use candid::Principal; +use hash256_std_hasher::Hash256StdHasher; +use hash_db::Hasher; +use serde::{Deserialize, Serialize}; +use trie_db::{proof::{generate_proof, verify_proof}, NodeCodec, TrieDBMut, TrieDBMutBuilder, TrieLayout, TrieMut}; + +use super::ProofOfChildren; + +mod layout; +mod backing; + +pub(super) type Hash = [u8; 32]; +pub(super) type ProofOfInclusion = Vec>; + +#[derive(Default, Debug, Clone, PartialEq)] +pub struct Blake3Hasher; + +impl Hasher for Blake3Hasher { + type Out = Hash; + + type StdHasher = Hash256StdHasher; + + const LENGTH: usize = 32; + + fn hash(x: &[u8]) -> Hash { + let mut hasher = blake3::Hasher::new(); + hasher.update(x); + let hs: [u8; 32] = hasher.finalize().into(); + hs + } +} + +pub(super) type ChildreenTreeLayout = layout::LayoutV1; + +#[derive(Serialize, Deserialize)] +pub struct ChildrenMerkle { + db: ChildrenBacking, + pub(super) root: Hash, + pub(super) proof_of_children: Option, +} + +impl Default for ChildrenMerkle { + fn default() -> Self { + Self { + db: ChildrenBacking::default(), + root: ::Codec::hashed_null_node(), + proof_of_children: None, + } + } +} + +impl ChildrenMerkle { + fn trie_mut(&mut self) -> TrieDBMut<'_, ChildreenTreeLayout> { + TrieDBMutBuilder::from_existing( + &mut self.db, + &mut self.root, + ).build() + } + + pub fn insert_children(&mut self, children: impl IntoIterator) { + let prev_root = self.root; + let mut trie = self.trie_mut(); + for child in children { + let key = Blake3Hasher::hash(child.as_slice()); + trie.insert(&key, b"_").expect("insertion should not fail"); + } + std::mem::drop(trie); + if self.root != prev_root { + // mark proof of children as stale + self.proof_of_children = None; + } + } + + pub fn remove_child(&mut self, child: Principal) { + let mut trie = self.trie_mut(); + let key = Blake3Hasher::hash(child.as_slice()); + trie.remove(&key).expect("removal should not fail"); + std::mem::drop(trie); + + // mark proof of children as stale + self.proof_of_children = None; + } + + pub fn proof_of_inclusion(&self, child: Principal) -> Result { + let key = Blake3Hasher::hash(child.as_slice()); + generate_proof::<_, ChildreenTreeLayout, _, _>( + &self.db, + &self.root, + [&key], + ).map_err(|e| format!("failed to generate proof of inclusion {e:?}")) + } + + pub fn verify_proof_of_inclusion(root: Hash, proof_of_inclusion: &[Vec], child: Principal) -> Result<(), String> { + let key = Blake3Hasher::hash(child.as_slice()); + verify_proof::( + &root, + proof_of_inclusion, + [&(key, Some(b"_"))] + ).map_err(|_| "invalid proof of inclusion".to_string()) + } +} diff --git a/src/lib/shared_utils/src/common/participant_crypto/mod.rs b/src/lib/shared_utils/src/common/participant_crypto/mod.rs new file mode 100644 index 00000000..2b105192 --- /dev/null +++ b/src/lib/shared_utils/src/common/participant_crypto/mod.rs @@ -0,0 +1,254 @@ +//! Utilities for creating and verifying proof that a given canister is a part of YRAL Backend canisters +mod types; +pub mod merkle; + +use std::{cell::RefCell, collections::HashMap, thread::LocalKey}; + +use candid::{CandidType, Principal}; +use ed25519_compact::{Signature, PublicKey}; +use merkle::{ChildrenMerkle, ProofOfInclusion}; +use types::{ManagementCanisterSchnorrPublicKeyReply, ManagementCanisterSchnorrPublicKeyRequest, ManagementCanisterSignatureReply, ManagementCanisterSignatureRequest, SchnorrAlgorithm, SchnorrKeyId}; +use serde::{Serialize, Deserialize}; + +const fn is_local() -> bool { + let Some(network) = std::option_env!("DFX_NETWORK") else { + return true; + }; + + match network.as_bytes() { + b"ic" => false, + b"local" => true, + _ => panic!("unknown `DFX_NETWORK`"), + } +} + +pub(crate) const THRESHOLD_SCHNORR_KEY: &str = if is_local() { + "dfx_test_key" +} else { + "key_1" +}; + +pub(crate) type LocalPoPStore = LocalKey>; + +#[derive(Default, Serialize, Deserialize)] +pub struct PubKeyCache(HashMap>); + +impl PubKeyCache { + async fn get_or_init_public_key(store: &'static LocalPoPStore, principal: Principal) -> Result { + let maybe_pk = store.with_borrow(|store| { + store.pubkey_cache().0.get(&principal).cloned() + }); + if let Some(pk) = maybe_pk { + return PublicKey::from_slice(pk.as_slice()) + .map_err(|_| "invalid public key".to_string()) + } + + let derive_args = ManagementCanisterSchnorrPublicKeyRequest { + derivation_path: vec![], + canister_id: Some(principal), + key_id: SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: THRESHOLD_SCHNORR_KEY.to_string(), + } + }; + let (key_res,): (ManagementCanisterSchnorrPublicKeyReply,) = ic_cdk::call( + Principal::management_canister(), + "schnorr_public_key", + (derive_args,) + ) + .await + .map_err(|(_, msg)| { + format!("unable to get public key: {msg}") + })?; + + let key = key_res.public_key; + let vk = PublicKey::from_slice(key.as_slice()) + .map_err(|_| "invalid public key".to_string())?; + store.with_borrow_mut(|store| { + store.pubkey_cache_mut().0.insert(principal, key.clone()); + }); + + Ok(vk) + } +} + +#[derive(Serialize)] +struct ProofOfAuthorityMsg { + prefix: &'static [u8], + pub merkle_root: [u8; 32], +} + +impl ProofOfAuthorityMsg { + pub fn new(merkle_root: [u8; 32]) -> Self { + Self { + prefix: b"CHILDREN", + merkle_root, + } + } + + pub fn serialize_cbor(&self) -> Vec { + let mut bytes = vec![]; + ciborium::into_writer(self, &mut bytes) + .expect("PoaMessage should serialize succesfully"); + + bytes + } +} + +/// Proof that a given merkle tree contains children of the parent canister +#[derive(Clone, CandidType, Serialize, Deserialize)] +struct ProofOfChildren { + merkle_root: [u8; 32], + signature: Vec, +} + +impl ProofOfChildren { + async fn new(merkle_root: [u8; 32]) -> Result { + let message = ProofOfAuthorityMsg::new(merkle_root); + let sign_args = ManagementCanisterSignatureRequest { + message: message.serialize_cbor(), + derivation_path: vec![], + key_id: SchnorrKeyId { + algorithm: SchnorrAlgorithm::Ed25519, + name: THRESHOLD_SCHNORR_KEY.to_string() + }, + }; + + let (sig_res,): (ManagementCanisterSignatureReply,) = ic_cdk::api::call::call_with_payment( + Principal::management_canister(), + "sign_with_schnorr", + (sign_args,), + 25_000_000_000, + ) + .await + .map_err(|(_, msg)| format!("unable to sign: {msg}"))?; + + Ok(Self { + merkle_root, + signature: sig_res.signature, + }) + } + + pub fn verify(&self, parent_key: &PublicKey) -> Result<(), String> { + let message = ProofOfAuthorityMsg::new(self.merkle_root); + let message_raw = message.serialize_cbor(); + + let sig = Signature::from_slice(&self.signature).map_err(|_| "invalid proof".to_string())?; + + parent_key.verify(&message_raw, &sig).map_err(|_| "invalid proof".to_string())?; + + Ok(()) + } +} + +// Proof that given canister id exists in the merkle tree containing the children of the parent canister +#[derive(Clone, CandidType, Serialize, Deserialize)] +struct ProofOfChild { + principal: Principal, + children_proof: ProofOfChildren, + proof_of_inclusion: ProofOfInclusion, +} + +impl ProofOfChild { + pub fn new(children_proof: ProofOfChildren, principal: Principal, proof_of_inclusion: ProofOfInclusion) -> Self { + Self { + principal, + children_proof, + proof_of_inclusion, + } + } + + pub fn verify(&self, parent_key: &PublicKey) -> Result<(), String> { + self.children_proof.verify(parent_key)?; + + ChildrenMerkle::verify_proof_of_inclusion( + self.children_proof.merkle_root, + &self.proof_of_inclusion, + self.principal, + )?; + + Ok(()) + } +} + +#[derive(Clone, CandidType, Serialize, Deserialize)] +pub struct ProofOfParticipation { + chain: Vec, +} + +impl ProofOfParticipation { + /// New PoP for platform orchestrator + pub fn new_for_root() -> Self { + Self { + chain: vec![], + } + } + + pub async fn derive_for_child(&self, store: &'static LocalPoPStore, child: Principal) -> Result { + let (proof_of_inclusion, maybe_poc) = store.with_borrow(|s| { + let children_merkle = s.children_merkle(); + children_merkle.proof_of_inclusion(child) + .map(|poi| { + (poi, children_merkle.proof_of_children.clone()) + }) + })?; + let poc = if let Some(poc) = maybe_poc { + poc + } else { + let root = store.with_borrow(|s| s.children_merkle().root); + let poc = ProofOfChildren::new(root).await?; + store.with_borrow_mut(|s| { + s.children_merkle_mut().proof_of_children = Some(poc.clone()); + }); + poc + }; + + let mut chain = self.chain.clone(); + chain.push(ProofOfChild::new( + poc, + child, + proof_of_inclusion, + )); + + Ok(ProofOfParticipation { + chain + }) + } + + /// Verify that the caller is a YRAL canister + pub async fn verify_caller_is_participant(&self, store: &'static LocalPoPStore) -> Result<(), String> { + if is_local() { + // Hack: Always pass on local testing node + // a proper implementation requires deploying platform orchestrator locally + return Ok(()) + } + + let platform_orchestrator = store.with_borrow(|s| s.platform_orchestrator()); + let canister = ic_cdk::caller(); + + let mut parent = PubKeyCache::get_or_init_public_key(store, platform_orchestrator).await?; + for proof in &self.chain { + proof.verify(&parent)?; + if proof.principal == canister { + return Ok(()) + } + parent = PubKeyCache::get_or_init_public_key(store, proof.principal).await?; + } + + Err("invalid proof".to_string()) + } +} + +pub trait ProofOfParticipationStore { + fn pubkey_cache(&self) -> &PubKeyCache; + + fn pubkey_cache_mut(&mut self) -> &mut PubKeyCache; + + fn platform_orchestrator(&self) -> Principal; +} + +pub trait ProofOfParticipationDeriverStore { + fn children_merkle(&self) -> &merkle::ChildrenMerkle; + + fn children_merkle_mut(&mut self) -> &mut merkle::ChildrenMerkle; +} diff --git a/src/lib/shared_utils/src/common/participant_crypto/types.rs b/src/lib/shared_utils/src/common/participant_crypto/types.rs new file mode 100644 index 00000000..41d2bd64 --- /dev/null +++ b/src/lib/shared_utils/src/common/participant_crypto/types.rs @@ -0,0 +1,41 @@ +use candid::{CandidType, Principal}; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Debug)] +pub struct ManagementCanisterSchnorrPublicKeyRequest { + pub canister_id: Option, + pub derivation_path: Vec>, + pub key_id: SchnorrKeyId, +} + +#[derive(CandidType, Deserialize, Debug)] +pub struct ManagementCanisterSchnorrPublicKeyReply { + pub public_key: Vec, + pub chain_code: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Debug, Copy, Clone)] +pub enum SchnorrAlgorithm { + #[serde(rename = "bip340secp256k1")] + Bip340Secp256k1, + #[serde(rename = "ed25519")] + Ed25519, +} + +#[derive(CandidType, Serialize, Debug, Clone)] +pub struct SchnorrKeyId { + pub algorithm: SchnorrAlgorithm, + pub name: String, +} + +#[derive(CandidType, Serialize, Debug)] +pub struct ManagementCanisterSignatureRequest { + pub message: Vec, + pub derivation_path: Vec>, + pub key_id: SchnorrKeyId, +} + +#[derive(CandidType, Deserialize, Debug)] +pub struct ManagementCanisterSignatureReply { + pub signature: Vec, +} \ No newline at end of file diff --git a/src/lib/test_utils/src/setup/env/pocket_ic_env.rs b/src/lib/test_utils/src/setup/env/pocket_ic_env.rs index 0fef26fa..f7311ab1 100644 --- a/src/lib/test_utils/src/setup/env/pocket_ic_env.rs +++ b/src/lib/test_utils/src/setup/env/pocket_ic_env.rs @@ -1,9 +1,9 @@ use std::collections::{HashMap, HashSet}; -use candid::{CandidType, Principal}; +use candid::{utils::ArgumentEncoder, CandidType, Deserialize, Principal}; use ic_cdk::api::management_canister::main::CanisterId; use ic_ledger_types::{AccountIdentifier, BlockIndex, Tokens, DEFAULT_SUBACCOUNT}; -use pocket_ic::{CanisterSettings, PocketIc, PocketIcBuilder}; +use pocket_ic::{CanisterSettings, PocketIc, PocketIcBuilder, UserError, WasmResult}; use shared_utils::{ canister_specific::platform_orchestrator::types::args::PlatformOrchestratorInitArgs, common::types::{ @@ -42,6 +42,7 @@ struct AuthorizedSubnetWorks { pub fn get_new_pocket_ic_env() -> (PocketIc, KnownPrincipalMap) { let pocket_ic = PocketIcBuilder::new() .with_nns_subnet() + .with_ii_subnet() // enables tSchnorr .with_application_subnet() .with_application_subnet() .with_system_subnet() @@ -192,3 +193,76 @@ pub fn get_new_pocket_ic_env() -> (PocketIc, KnownPrincipalMap) { (pocket_ic, known_principal) } + +pub fn execute_query Deserialize<'x>>( + pic: &PocketIc, + sender: Principal, + canister_id: CanisterId, + method_name: &str, + payload: &P, +) -> R { + unwrap_res(pic.query_call(canister_id, sender, method_name, candid::encode_one(payload).unwrap())) +} + +pub fn execute_query_multi Deserialize<'x>>( + pic: &PocketIc, + sender: Principal, + canister_id: CanisterId, + method_name: &str, + payload: P, +) -> R { + unwrap_res(pic.query_call(canister_id, sender, method_name, candid::encode_args(payload).unwrap())) +} + +pub fn execute_update Deserialize<'x>>( + pic: &PocketIc, + sender: Principal, + canister_id: CanisterId, + method_name: &str, + payload: &P, +) -> R { + unwrap_res(pic.update_call(canister_id, sender, method_name, candid::encode_one(payload).unwrap())) +} + +pub fn execute_update_multi Deserialize<'x>>( + pic: &PocketIc, + sender: Principal, + canister_id: CanisterId, + method: &str, + payload: P +) -> R { + unwrap_res(pic.update_call(canister_id, sender, method, candid::encode_args(payload).unwrap())) +} + +pub fn execute_update_no_res( + pic: &PocketIc, + sender: Principal, + canister_id: CanisterId, + method: &str, + payload: &P, +) { + let res = pic.update_call(canister_id, sender, method, candid::encode_one(payload).unwrap()); + if let WasmResult::Reject(error) = res.unwrap() { + panic!("{error}"); + } +} + +pub fn execute_update_no_res_multi( + pic: &PocketIc, + sender: Principal, + canister_id: CanisterId, + method: &str, + payload: P, +) { + let res = pic.update_call(canister_id, sender, method, candid::encode_args(payload).unwrap()); + if let WasmResult::Reject(error) = res.unwrap() { + panic!("{error}"); + } +} + +fn unwrap_res Deserialize<'x>>(response: Result) -> R { + match response.unwrap() { + WasmResult::Reply(bytes) => candid::decode_one(&bytes).unwrap(), + WasmResult::Reject(error) => panic!("{error}"), + } +} diff --git a/src/lib/test_utils/src/setup/env/v1.rs b/src/lib/test_utils/src/setup/env/v1.rs index e7bf3e45..2d3aed7a 100644 --- a/src/lib/test_utils/src/setup/env/v1.rs +++ b/src/lib/test_utils/src/setup/env/v1.rs @@ -121,6 +121,8 @@ pub fn get_initialized_env_with_provisioned_known_canisters( known_principal_ids: Some(known_principal_map_with_all_canisters.clone()), access_control_map: Some(user_index_access_control_map), version: String::from("v1.0.0"), + // V1 tests dont rely on PoP + proof_of_participation: None, }) .unwrap(), );