From 011259a522b8216dfbedb835681401596a5f68fa Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 16 Jun 2026 19:51:59 +0200 Subject: [PATCH 01/15] chore: migrate rust/composite_query to icp-cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace dfx.json with icp.yaml using @dfinity/rust@v3.3.0 - Rename canisters: kv_frontend → caller, data_partition → callee - Move each canister into its own subdirectory (caller/, callee/) - Update workspace Cargo.toml to reference new members - Rewrite caller/lib.rs: replace ic_cdk::api::management_canister with ic-cdk-management-canister crate, replace ic_cdk::api::call::call with ic_cdk::call::Call::bounded_wait pattern - Add Makefile with 7 tests covering put, composite query get, get_update, null lookup, multi-partition routing, and lookup composite query - Add rust-toolchain.toml with wasm32-unknown-unknown target - Add rust-composite_query job to composite_query.yml workflow - Delete legacy rust-composite_query-example.yml workflow - Update README with icp-cli deploy instructions and architecture diagram Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/composite_query.yml | 19 +- .../rust-composite_query-example.yml | 53 -- rust/composite_query/Cargo.lock | 890 ------------------ rust/composite_query/Cargo.toml | 6 +- rust/composite_query/Makefile | 55 ++ rust/composite_query/README.md | 73 +- rust/composite_query/callee/Cargo.toml | 13 + .../{src/data_partition => callee}/lib.rs | 7 +- rust/composite_query/caller/Cargo.toml | 13 + rust/composite_query/caller/lib.rs | 138 +++ rust/composite_query/dfx.json | 21 - rust/composite_query/icp.yaml | 15 + rust/composite_query/rust-toolchain.toml | 2 + rust/composite_query/src/data_partition.did | 4 - .../src/data_partition/Cargo.toml | 15 - rust/composite_query/src/kv_frontend.did | 6 - .../src/kv_frontend/Cargo.toml | 14 - rust/composite_query/src/kv_frontend/lib.rs | 121 --- 18 files changed, 293 insertions(+), 1172 deletions(-) delete mode 100644 .github/workflows/rust-composite_query-example.yml delete mode 100644 rust/composite_query/Cargo.lock create mode 100644 rust/composite_query/Makefile create mode 100644 rust/composite_query/callee/Cargo.toml rename rust/composite_query/{src/data_partition => callee}/lib.rs (72%) create mode 100644 rust/composite_query/caller/Cargo.toml create mode 100644 rust/composite_query/caller/lib.rs delete mode 100644 rust/composite_query/dfx.json create mode 100644 rust/composite_query/icp.yaml create mode 100644 rust/composite_query/rust-toolchain.toml delete mode 100644 rust/composite_query/src/data_partition.did delete mode 100644 rust/composite_query/src/data_partition/Cargo.toml delete mode 100644 rust/composite_query/src/kv_frontend.did delete mode 100644 rust/composite_query/src/kv_frontend/Cargo.toml delete mode 100644 rust/composite_query/src/kv_frontend/lib.rs diff --git a/.github/workflows/composite_query.yml b/.github/workflows/composite_query.yml index 4388649077..ead28e2f74 100644 --- a/.github/workflows/composite_query.yml +++ b/.github/workflows/composite_query.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - motoko/composite_query/** + - rust/composite_query/** - .github/workflows/composite_query.yml concurrency: @@ -15,7 +16,7 @@ concurrency: jobs: motoko-composite_query: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -25,4 +26,18 @@ jobs: run: | icp network start -d icp deploy --cycles 30t - bash test.sh + make test + + rust-composite_query: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.0 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/composite_query + run: | + icp network start -d + icp deploy --cycles 30t + make test diff --git a/.github/workflows/rust-composite_query-example.yml b/.github/workflows/rust-composite_query-example.yml deleted file mode 100644 index c676c6c284..0000000000 --- a/.github/workflows/rust-composite_query-example.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: rust-composite_query -on: - push: - branches: - - master - pull_request: - paths: - - rust/composite_query/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/rust-composite_query-example.yml - - .github/workflows/rust-composite_query-skip.yml - - .ic-commit -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - rust-composite_query-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Rust composite_query Darwin - run: | - pushd rust/composite_query - dfx start --background - dfx canister create data_partition --no-wallet - dfx build data_partition - dfx canister create kv_frontend --no-wallet - dfx build kv_frontend - dfx canister install kv_frontend - dfx canister call kv_frontend put '(1, 1337)' - dfx canister call kv_frontend get '(1)' | grep '1_337' - popd - rust-composite_query-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Rust composite_query Linux - run: | - pushd rust/composite_query - dfx start --background - dfx canister create data_partition --no-wallet - dfx build data_partition - dfx canister create kv_frontend --no-wallet - dfx build kv_frontend - dfx canister install kv_frontend - dfx canister call kv_frontend put '(1, 1337)' - dfx canister call kv_frontend get '(1)' | grep '1_337' - popd diff --git a/rust/composite_query/Cargo.lock b/rust/composite_query/Cargo.lock deleted file mode 100644 index fc0198930a..0000000000 --- a/rust/composite_query/Cargo.lock +++ /dev/null @@ -1,890 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "binaryen" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783bea139d75b6a565b13fab54d12ec4d58724a9458598ad7283d578f4f8777a" -dependencies = [ - "binaryen-sys", -] - -[[package]] -name = "binaryen-sys" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e9636d01b92f2df45dce29c35a9e3724687c1055bb4472fb4b829cc4a5a561" -dependencies = [ - "cc", - "cmake", - "heck 0.3.3", - "regex", -] - -[[package]] -name = "binread" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" -dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", -] - -[[package]] -name = "binread_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "candid" -version = "0.10.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253bab4a9be502c82332b60cbeee6202ad0692834efeec95fae9f29db33d692" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror", -] - -[[package]] -name = "candid_derive" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de398570c386726e7a59d9887b68763c481477f9a043fb998a2e09d428df1a9" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "cc" -version = "1.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap_derive" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "data-encoding" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" - -[[package]] -name = "data_partition" -version = "1.0.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-macros", - "ic-cdk-optimizer", - "ic-stable-structures", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "glob" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "humansize" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" - -[[package]] -name = "ic-cdk" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122efbcb0af5280d408a75a57b7dc6e9d92893bf6ed9cc98fe4dcff51f18b67c" -dependencies = [ - "candid", - "ic-cdk-macros", - "ic0", - "serde", - "serde_bytes", -] - -[[package]] -name = "ic-cdk-macros" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c792bf0d1621c893ccf2bcdeac4ee70121103a03030a1827031a6b3c60488944" -dependencies = [ - "candid", - "proc-macro2", - "quote", - "serde", - "serde_tokenstream", - "syn 2.0.48", -] - -[[package]] -name = "ic-cdk-optimizer" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c8a7064d878355842d64c20a27c68cfee191781cd3ba5b532974ce6f07e05e" -dependencies = [ - "binaryen", - "clap", - "humansize", - "wabt", -] - -[[package]] -name = "ic-stable-structures" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95dce29e3ceb0e6da3e78b305d95365530f2efd2146ca18590c0ef3aa6038568" - -[[package]] -name = "ic0" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de254dd67bbd58073e23dc1c8553ba12fa1dc610a19de94ad2bbcd0460c067f" - -[[package]] -name = "ic_principal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2", - "thiserror", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" - -[[package]] -name = "kv_frontend" -version = "1.0.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-macros", - "ic-cdk-optimizer", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "memchr" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - -[[package]] -name = "num-bigint" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "pretty" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.78" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" -dependencies = [ - "cc", -] - -[[package]] -name = "quote" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex" -version = "1.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - -[[package]] -name = "ryu" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" - -[[package]] -name = "serde" -version = "1.0.196" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.196" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "serde_json" -version = "1.0.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_tokenstream" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.48", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "stacker" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - -[[package]] -name = "thiserror" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wabt" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bef93d5e6c81a293bccf107cf43aa47239382f455ba14869d36695d8963b9c" -dependencies = [ - "serde", - "serde_derive", - "serde_json", - "wabt-sys", -] - -[[package]] -name = "wabt-sys" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a4e043159f63e16986e713e9b5e1c06043df4848565bf672e27c523864c7791" -dependencies = [ - "cc", - "cmake", - "glob", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/rust/composite_query/Cargo.toml b/rust/composite_query/Cargo.toml index d2ca148272..10b4a1ad9f 100644 --- a/rust/composite_query/Cargo.toml +++ b/rust/composite_query/Cargo.toml @@ -1,5 +1,3 @@ [workspace] -members = [ - "src/data_partition", - "src/kv_frontend" -] \ No newline at end of file +members = ["caller", "callee"] +resolver = "2" diff --git a/rust/composite_query/Makefile b/rust/composite_query/Makefile new file mode 100644 index 0000000000..2fdeb12371 --- /dev/null +++ b/rust/composite_query/Makefile @@ -0,0 +1,55 @@ +.PHONY: test topup + +topup: + icp canister top-up --amount 30t caller + +test: + @echo "=== Test 1: put inserts a key-value pair (also creates callee partitions) ===" + @result=$$(icp canister call caller put '(1 : nat, 1337 : nat)') && \ + echo "$$result" && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2: put a second value for the same key returns the old value ===" + @result=$$(icp canister call caller put '(1 : nat, 42 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q '1_337' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3: get (composite query) retrieves the stored value ===" + @result=$$(icp canister call --query caller get '(1 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'opt (42' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 4: get_update returns the same value via an update call ===" + @result=$$(icp canister call caller get_update '(1 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'opt (42' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 5: get returns null for a missing key ===" + @result=$$(icp canister call --query caller get '(99 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'null' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 6: composite query routes correctly across partitions ===" + @icp canister call caller put '(2 : nat, 100 : nat)' && \ + icp canister call caller put '(3 : nat, 200 : nat)' && \ + icp canister call caller put '(4 : nat, 300 : nat)' && \ + result1=$$(icp canister call --query caller get '(1 : nat)') && \ + result2=$$(icp canister call --query caller get '(2 : nat)') && \ + result3=$$(icp canister call --query caller get '(3 : nat)') && \ + result4=$$(icp canister call --query caller get '(4 : nat)') && \ + echo "$$result1" && echo "$$result2" && echo "$$result3" && echo "$$result4" && \ + echo "$$result1" | grep -q 'opt (42' && \ + echo "$$result2" | grep -q 'opt (100' && \ + echo "$$result3" | grep -q 'opt (200' && \ + echo "$$result4" | grep -q 'opt (300' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 7: lookup (query) returns the partition index and canister ID ===" + @result=$$(icp canister call --query caller lookup '(1 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q '1 :' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index f4d7f287bf..98f088670c 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -1,62 +1,57 @@ # Composite queries -## Prerequisites -This example requires an installation of: +On the Internet Computer, regular query functions are fast (no consensus) but have one strict limitation: **they cannot call other canisters**. Composite queries lift this restriction — a `#[query(composite = true)]` function can call query methods on other canisters while keeping the speed benefit of a query call. -- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx). -- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples` +For more background see [Composite queries](https://docs.internetcomputer.org/guides/canister-calls/parallel-inter-canister-calls/#composite-queries) in the ICP developer docs. -Begin by opening a terminal window. +This example implements a distributed key-value store (`caller`) that shards its entries across five dynamically-installed `callee` child canisters. Looking up a key requires calling the appropriate callee: -## Step 1: Setup the project environment +- `get(k)` — **composite query**: delegates to the correct `callee.get(k)` as a cross-canister query call. Fast, no consensus. +- `get_update(k)` — **update call**: same lookup, but via an update call to the callee. Slower (goes through consensus) but provided here for comparison. -We first need to build the data partition backend canister. +Both functions return the same result; the difference is latency and call semantics. -```bash -cd examples/rust/composite_query -dfx start --background -dfx canister create data_partition --no-wallet -dfx build data_partition +## Architecture + +``` +caller + n = 5 callees ┌── callee 0 (keys 0, 5, 10, …) + key % n routes ────┼── callee 1 (keys 1, 6, 11, …) + ├── callee 2 (keys 2, 7, 12, …) + ├── callee 3 (keys 3, 8, 13, …) + └── callee 4 (keys 4, 9, 14, …) ``` -During the compilation of the fronted canister, the canister's wasm code will be inlined in the frontend canister's wasm code. +`caller.put(k, v)` dynamically installs a `callee` canister if one does not exist for `k % 5`, then stores the entry there via an update call. `caller.get(k)` and `caller.get_update(k)` both route to the same callee via `k % 5`. -```bash -dfx canister create kv_frontend --no-wallet -dfx build kv_frontend -dfx canister install kv_frontend -``` +The `callee` WASM is embedded directly into the `caller` WASM binary at compile time via `include_bytes!`. This means only the `caller` canister needs to be deployed — it installs the `callee` canisters programmatically on first use. -## Step 2: Using the canister +## Build and deploy from the command line -Now we add some key value pairs via the frontend canister. +### Prerequisites -```bash -dfx canister call kv_frontend put '(1:nat, 1337:nat)' -(null) -dfx canister call kv_frontend put '(1:nat, 42:nat)' -(opt (1_337 : nat)) -``` +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -Note that the first call to `put` is slow, since the data partitions have to be created first. +### Install ```bash -dfx canister call kv_frontend get '(1:nat)' -(opt (42 : nat)) +git clone https://github.com/dfinity/examples +cd examples/rust/composite_query ``` -We can also query it via a (duplicate) method by doing update calls: +### Deploy and test ```bash -dfx canister call kv_frontend get_update '(1:nat)' -(opt (42 : nat)) +icp network start -d +icp deploy --cycles 30t +make test +icp network stop ``` -It's also possible to do *two* query calls, first into the frontend and then into the data partition: +> `icp deploy --cycles 30t` is required because `caller` dynamically creates `callee` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `make topup`. -```bash -$ dfx canister call kv_frontend lookup '(1: nat)' -(1 : nat, "dmalx-m4aaa-aaaaa-qaanq-cai") -$ dfx canister call dmalx-m4aaa-aaaaa-qaanq-cai get '(1: nat)' --query -(42 : nat) -``` +Note that the first call to `put` is slow, since all five callee partitions are created at that point. + +## Security considerations and best practices + +Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app. diff --git a/rust/composite_query/callee/Cargo.toml b/rust/composite_query/callee/Cargo.toml new file mode 100644 index 0000000000..cce59b7e97 --- /dev/null +++ b/rust/composite_query/callee/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "callee" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" +ic-stable-structures = "0.6" diff --git a/rust/composite_query/src/data_partition/lib.rs b/rust/composite_query/callee/lib.rs similarity index 72% rename from rust/composite_query/src/data_partition/lib.rs rename to rust/composite_query/callee/lib.rs index c7c697ff91..aeec77db0f 100644 --- a/rust/composite_query/src/data_partition/lib.rs +++ b/rust/composite_query/callee/lib.rs @@ -1,4 +1,5 @@ -use ic_cdk_macros::{query, update}; +use ic_cdk::update; +use ic_cdk::query; use ic_stable_structures::{BTreeMap, DefaultMemoryImpl}; use std::cell::RefCell; @@ -9,7 +10,7 @@ thread_local! { #[update] fn put(key: u128, value: u128) -> Option { - ic_cdk::println!("Set in backend for key={} with value={}", key, value); + ic_cdk::println!("Set in callee for key={} with value={}", key, value); STORE.with(|store| store.borrow_mut().insert(key, value)) } @@ -17,7 +18,7 @@ fn put(key: u128, value: u128) -> Option { fn get(key: u128) -> Option { STORE.with(|store| { let r = store.borrow().get(&key); - ic_cdk::println!("Get in backend for key={} - result={:?}", key, r); + ic_cdk::println!("Get in callee for key={} - result={:?}", key, r); r }) } diff --git a/rust/composite_query/caller/Cargo.toml b/rust/composite_query/caller/Cargo.toml new file mode 100644 index 0000000000..9b999d1f15 --- /dev/null +++ b/rust/composite_query/caller/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "caller" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" +ic-cdk-management-canister = "0.1.1" diff --git a/rust/composite_query/caller/lib.rs b/rust/composite_query/caller/lib.rs new file mode 100644 index 0000000000..867c498c1c --- /dev/null +++ b/rust/composite_query/caller/lib.rs @@ -0,0 +1,138 @@ +use candid::Principal; +use ic_cdk::call::Call; +use ic_cdk::query; +use ic_cdk::update; +use ic_cdk_management_canister::{ + CanisterInstallMode, CanisterSettings, CreateCanisterArgs, InstallCodeArgs, + create_canister_with_extra_cycles, install_code, +}; + +use std::sync::Arc; +use std::sync::RwLock; + +const NUM_PARTITIONS: usize = 5; + +// Inline wasm binary of the callee canister +pub const WASM: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/callee.wasm"); + +thread_local! { + // A list of canister IDs for data partitions + static CANISTER_IDS: Arc>> = Arc::new(RwLock::new(vec![])); +} + +#[update] +async fn put(key: u128, value: u128) -> Option { + // Create partitions if they don't exist yet + if CANISTER_IDS.with(|canister_ids| canister_ids.read().unwrap().is_empty()) { + for _ in 0..NUM_PARTITIONS { + create_partition_canister().await; + } + } + + let canister_id = get_partition_for_key(key); + ic_cdk::println!( + "Put in caller for key={} .. using callee={}", + key, + canister_id.to_text() + ); + + Call::bounded_wait(canister_id, "put") + .with_args(&(key, value)) + .await + .ok() + .and_then(|r| r.candid::<(Option,)>().ok()) + .and_then(|(v,)| v) +} + +#[query(composite = true)] +async fn get(key: u128) -> Option { + let canister_id = get_partition_for_key(key); + ic_cdk::println!( + "Get in caller for key={} .. using callee={}", + key, + canister_id.to_text() + ); + + Call::bounded_wait(canister_id, "get") + .with_arg(key) + .await + .ok() + .and_then(|r| r.candid::<(Option,)>().ok()) + .and_then(|(v,)| v) +} + +#[update] +async fn get_update(key: u128) -> Option { + let canister_id = get_partition_for_key(key); + ic_cdk::println!( + "Get as update in caller for key={} .. using callee={}", + key, + canister_id.to_text() + ); + + Call::bounded_wait(canister_id, "get") + .with_arg(key) + .await + .ok() + .and_then(|r| r.candid::<(Option,)>().ok()) + .and_then(|(v,)| v) +} + +fn get_partition_for_key(key: u128) -> Principal { + CANISTER_IDS.with(|canister_ids| { + let canister_ids = canister_ids.read().unwrap(); + canister_ids[lookup(key).0 as usize] + }) +} + +#[query(composite = true)] +fn lookup(key: u128) -> (u128, String) { + let r = key % NUM_PARTITIONS as u128; + ( + r, + CANISTER_IDS.with(|canister_ids| { + let canister_ids = canister_ids.read().unwrap(); + canister_ids[r as usize].to_text() + }), + ) +} + +async fn create_partition_canister() { + const T: u128 = 1_000_000_000_000; + + let create_args = CreateCanisterArgs { + settings: Some(CanisterSettings { + controllers: Some(vec![ic_cdk::canister_self()]), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: None, + reserved_cycles_limit: None, + log_visibility: None, + log_memory_limit: None, + wasm_memory_limit: None, + wasm_memory_threshold: None, + environment_variables: None, + }), + }; + + let result = create_canister_with_extra_cycles(&create_args, 10 * T) + .await + .unwrap(); + let canister_id = result.canister_id; + + ic_cdk::println!("Created callee canister {}", canister_id); + + let install_args = InstallCodeArgs { + mode: CanisterInstallMode::Install, + canister_id, + wasm_module: WASM.to_vec(), + arg: vec![], + }; + + install_code(&install_args).await.unwrap(); + + CANISTER_IDS.with(|canister_ids| { + canister_ids.write().unwrap().push(canister_id); + }); +} diff --git a/rust/composite_query/dfx.json b/rust/composite_query/dfx.json deleted file mode 100644 index c056ca0ff8..0000000000 --- a/rust/composite_query/dfx.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "canisters": { - "data_partition": { - "candid": "src/data_partition.did", - "package": "data_partition", - "type": "rust" - }, - "kv_frontend": { - "candid": "src/kv_frontend.did", - "package": "kv_frontend", - "type": "rust" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "version": 1 -} diff --git a/rust/composite_query/icp.yaml b/rust/composite_query/icp.yaml new file mode 100644 index 0000000000..09d4a7530c --- /dev/null +++ b/rust/composite_query/icp.yaml @@ -0,0 +1,15 @@ +networks: + - name: local + mode: managed + +canisters: + - name: caller + recipe: + type: "@dfinity/rust@v3.3.0" + configuration: + package: caller + - name: callee + recipe: + type: "@dfinity/rust@v3.3.0" + configuration: + package: callee diff --git a/rust/composite_query/rust-toolchain.toml b/rust/composite_query/rust-toolchain.toml new file mode 100644 index 0000000000..990104f055 --- /dev/null +++ b/rust/composite_query/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +targets = ["wasm32-unknown-unknown"] diff --git a/rust/composite_query/src/data_partition.did b/rust/composite_query/src/data_partition.did deleted file mode 100644 index 5b0d356187..0000000000 --- a/rust/composite_query/src/data_partition.did +++ /dev/null @@ -1,4 +0,0 @@ -service : { - "put" : (nat, nat) -> (opt nat); - "get" : (nat) -> (opt nat) query; -}; diff --git a/rust/composite_query/src/data_partition/Cargo.toml b/rust/composite_query/src/data_partition/Cargo.toml deleted file mode 100644 index 6e20a22cf5..0000000000 --- a/rust/composite_query/src/data_partition/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "data_partition" -version = "1.0.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] -path = "lib.rs" - -[dependencies] -candid = "0.10" -ic-cdk = "0.17.1" -ic-cdk-macros = "0.17.1" -ic-cdk-optimizer = "0.3.5" -ic-stable-structures = "0.5.4" diff --git a/rust/composite_query/src/kv_frontend.did b/rust/composite_query/src/kv_frontend.did deleted file mode 100644 index a256163109..0000000000 --- a/rust/composite_query/src/kv_frontend.did +++ /dev/null @@ -1,6 +0,0 @@ -service : { - "put" : (nat, nat) -> (opt nat); - "get" : (nat) -> (opt nat) composite_query; - "get_update" : (nat) -> (opt nat); - "lookup" : (nat) -> (nat, text) query; -}; diff --git a/rust/composite_query/src/kv_frontend/Cargo.toml b/rust/composite_query/src/kv_frontend/Cargo.toml deleted file mode 100644 index f215f2a1ad..0000000000 --- a/rust/composite_query/src/kv_frontend/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "kv_frontend" -version = "1.0.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] -path = "lib.rs" - -[dependencies] -candid = "0.10" -ic-cdk = "0.17.1" -ic-cdk-macros = "0.17.1" -ic-cdk-optimizer = "0.3.5" diff --git a/rust/composite_query/src/kv_frontend/lib.rs b/rust/composite_query/src/kv_frontend/lib.rs deleted file mode 100644 index f2361b55c2..0000000000 --- a/rust/composite_query/src/kv_frontend/lib.rs +++ /dev/null @@ -1,121 +0,0 @@ -use ic_cdk::api::call::{call}; -use ic_cdk::api::management_canister::main::{CreateCanisterArgument, create_canister, InstallCodeArgument, install_code, CanisterInstallMode}; -use ic_cdk::api::management_canister::provisional::CanisterSettings; -use ic_cdk_macros::{query, update}; -use candid::Principal; - -use std::sync::Arc; -use std::sync::RwLock; - -const NUM_PARTITIONS: usize = 5; - -// Inline wasm binary of data partition canister -pub const WASM: &[u8] = - include_bytes!("../../target/wasm32-unknown-unknown/release/data_partition.wasm"); - -thread_local! { - // A list of canister IDs for data partitions - static CANISTER_IDS: Arc>> = Arc::new(RwLock::new(vec![])); -} - -#[update] -async fn put(key: u128, value: u128) -> Option { - - // Create partitions if they don't exist yet - if CANISTER_IDS.with(|canister_ids| { - let canister_ids = canister_ids.read().unwrap(); - canister_ids.len() == 0 - }) { - for _ in 0..NUM_PARTITIONS { - create_data_partition_canister_from_wasm().await; - } - } - - let canister_id = get_partition_for_key(key); - ic_cdk::println!("Put in frontend for key={} .. using backend={}", key, canister_id.to_text()); - match call(canister_id, "put", (key, value), ).await { - Ok(r) => { - let (res,): (Option,) = r; - res - }, - Err(_) => None, - } -} - -#[query(composite = true)] -async fn get(key: u128) -> Option { - let canister_id = get_partition_for_key(key); - ic_cdk::println!("Get in frontend for key={} .. using backend={}", key, canister_id.to_text()); - match call(canister_id, "get", (key, ), ).await { - Ok(r) => { - let (res,): (Option,) = r; - res - }, - Err(_) => None, - } -} - -#[update] -async fn get_update(key: u128) -> Option { - let canister_id = get_partition_for_key(key); - ic_cdk::println!("Get as update in frontend for key={} .. using backend={}", key, canister_id.to_text()); - match call(canister_id, "get", (key, ), ).await { - Ok(r) => { - let (res,): (Option,) = r; - res - }, - Err(_) => None, - } -} - -fn get_partition_for_key(key: u128) -> Principal { - let canister_id = CANISTER_IDS.with(|canister_ids| { - let canister_ids = canister_ids.read().unwrap(); - canister_ids[lookup(key).0 as usize] - }); - canister_id -} - -#[query(composite = true)] -fn lookup(key: u128) -> (u128, String) { - let r = key % NUM_PARTITIONS as u128; - (r, CANISTER_IDS.with(|canister_ids| { - let canister_ids = canister_ids.read().unwrap(); - canister_ids[r as usize].to_text() - })) -} - -async fn create_data_partition_canister_from_wasm() { - let create_args = CreateCanisterArgument { - settings: Some(CanisterSettings { - controllers: Some(vec![ic_cdk::id()]), - compute_allocation: None, - memory_allocation: None, - freezing_threshold: None, - reserved_cycles_limit: None, - log_visibility: None, - wasm_memory_limit: None, - }) - }; - - const T: u128 = 1_000_000_000_000; - - let canister_record = create_canister(create_args, 10 * T).await.unwrap(); - let canister_id = canister_record.0.canister_id; - - ic_cdk::println!("Created canister {}", canister_id); - - let install_args = InstallCodeArgument { - mode: CanisterInstallMode::Install, - canister_id, - wasm_module: WASM.to_vec(), - arg: vec![], - }; - - install_code(install_args).await.unwrap(); - - CANISTER_IDS.with(|canister_ids| { - let mut canister_ids = canister_ids.write().unwrap(); - canister_ids.push(canister_id); - }); -} From b5ab02d82bc41d0674b105cbe8a787e2f02b89b7 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 19:00:07 +0200 Subject: [PATCH 02/15] chore(rust/composite_query): test.sh over Makefile, bump CI image to 1.0.1 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/composite_query.yml | 8 ++-- rust/composite_query/Makefile | 55 --------------------------- rust/composite_query/README.md | 2 +- rust/composite_query/test.sh | 52 +++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 60 deletions(-) delete mode 100644 rust/composite_query/Makefile create mode 100755 rust/composite_query/test.sh diff --git a/.github/workflows/composite_query.yml b/.github/workflows/composite_query.yml index ead28e2f74..9a99937ce8 100644 --- a/.github/workflows/composite_query.yml +++ b/.github/workflows/composite_query.yml @@ -16,7 +16,7 @@ concurrency: jobs: motoko-composite_query: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 + container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -26,11 +26,11 @@ jobs: run: | icp network start -d icp deploy --cycles 30t - make test + bash test.sh rust-composite_query: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-rust:1.0.0 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -40,4 +40,4 @@ jobs: run: | icp network start -d icp deploy --cycles 30t - make test + bash test.sh diff --git a/rust/composite_query/Makefile b/rust/composite_query/Makefile deleted file mode 100644 index 2fdeb12371..0000000000 --- a/rust/composite_query/Makefile +++ /dev/null @@ -1,55 +0,0 @@ -.PHONY: test topup - -topup: - icp canister top-up --amount 30t caller - -test: - @echo "=== Test 1: put inserts a key-value pair (also creates callee partitions) ===" - @result=$$(icp canister call caller put '(1 : nat, 1337 : nat)') && \ - echo "$$result" && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 2: put a second value for the same key returns the old value ===" - @result=$$(icp canister call caller put '(1 : nat, 42 : nat)') && \ - echo "$$result" && \ - echo "$$result" | grep -q '1_337' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 3: get (composite query) retrieves the stored value ===" - @result=$$(icp canister call --query caller get '(1 : nat)') && \ - echo "$$result" && \ - echo "$$result" | grep -q 'opt (42' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 4: get_update returns the same value via an update call ===" - @result=$$(icp canister call caller get_update '(1 : nat)') && \ - echo "$$result" && \ - echo "$$result" | grep -q 'opt (42' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 5: get returns null for a missing key ===" - @result=$$(icp canister call --query caller get '(99 : nat)') && \ - echo "$$result" && \ - echo "$$result" | grep -q 'null' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 6: composite query routes correctly across partitions ===" - @icp canister call caller put '(2 : nat, 100 : nat)' && \ - icp canister call caller put '(3 : nat, 200 : nat)' && \ - icp canister call caller put '(4 : nat, 300 : nat)' && \ - result1=$$(icp canister call --query caller get '(1 : nat)') && \ - result2=$$(icp canister call --query caller get '(2 : nat)') && \ - result3=$$(icp canister call --query caller get '(3 : nat)') && \ - result4=$$(icp canister call --query caller get '(4 : nat)') && \ - echo "$$result1" && echo "$$result2" && echo "$$result3" && echo "$$result4" && \ - echo "$$result1" | grep -q 'opt (42' && \ - echo "$$result2" | grep -q 'opt (100' && \ - echo "$$result3" | grep -q 'opt (200' && \ - echo "$$result4" | grep -q 'opt (300' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 7: lookup (query) returns the partition index and canister ID ===" - @result=$$(icp canister call --query caller lookup '(1 : nat)') && \ - echo "$$result" && \ - echo "$$result" | grep -q '1 :' && \ - echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index 98f088670c..1c1c4f8fc5 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -44,7 +44,7 @@ cd examples/rust/composite_query ```bash icp network start -d icp deploy --cycles 30t -make test +bash test.sh icp network stop ``` diff --git a/rust/composite_query/test.sh b/rust/composite_query/test.sh new file mode 100755 index 0000000000..2147242f3d --- /dev/null +++ b/rust/composite_query/test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e + +echo "=== Test 1: put inserts a key-value pair (also creates callee partitions) ===" +result=$(icp canister call caller put '(1 : nat, 1337 : nat)') && \ + echo "$result" && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 2: put a second value for the same key returns the old value ===" +result=$(icp canister call caller put '(1 : nat, 42 : nat)') && \ + echo "$result" && \ + echo "$result" | grep -q '1_337' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 3: get (composite query) retrieves the stored value ===" +result=$(icp canister call --query caller get '(1 : nat)') && \ + echo "$result" && \ + echo "$result" | grep -q 'opt (42' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 4: get_update returns the same value via an update call ===" +result=$(icp canister call caller get_update '(1 : nat)') && \ + echo "$result" && \ + echo "$result" | grep -q 'opt (42' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 5: get returns null for a missing key ===" +result=$(icp canister call --query caller get '(99 : nat)') && \ + echo "$result" && \ + echo "$result" | grep -q 'null' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 6: composite query routes correctly across partitions ===" +icp canister call caller put '(2 : nat, 100 : nat)' && \ + icp canister call caller put '(3 : nat, 200 : nat)' && \ + icp canister call caller put '(4 : nat, 300 : nat)' && \ + result1=$(icp canister call --query caller get '(1 : nat)') && \ + result2=$(icp canister call --query caller get '(2 : nat)') && \ + result3=$(icp canister call --query caller get '(3 : nat)') && \ + result4=$(icp canister call --query caller get '(4 : nat)') && \ + echo "$result1" && echo "$result2" && echo "$result3" && echo "$result4" && \ + echo "$result1" | grep -q 'opt (42' && \ + echo "$result2" | grep -q 'opt (100' && \ + echo "$result3" | grep -q 'opt (200' && \ + echo "$result4" | grep -q 'opt (300' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 7: lookup (query) returns the partition index and canister ID ===" +result=$(icp canister call --query caller lookup '(1 : nat)') && \ + echo "$result" && \ + echo "$result" | grep -q '1 :' && \ + echo "PASS" || (echo "FAIL" && exit 1) From b042272593f1507e012f40aefb7acdb0ebdabe7c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 09:53:52 +0200 Subject: [PATCH 03/15] fix(rust/composite_query): custom build steps, export_candid, README fixes - Replace recipe with custom build steps: callee is built first so its WASM is available for include_bytes! in the caller. Only caller is deployed; callee is a compile-time dependency, not a deployed canister. - Add ic_cdk::export_candid!() to both caller and callee - Remove redundant networks block from icp.yaml - Add Node.js to README prerequisites - Fix make topup -> icp canister top-up --amount 30t caller Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/README.md | 3 ++- rust/composite_query/callee/lib.rs | 2 ++ rust/composite_query/caller/lib.rs | 2 ++ rust/composite_query/icp.yaml | 32 ++++++++++++++++++------------ 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index 1c1c4f8fc5..7eb7952561 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -30,6 +30,7 @@ The `callee` WASM is embedded directly into the `caller` WASM binary at compile ### Prerequisites +- Node.js - icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` ### Install @@ -48,7 +49,7 @@ bash test.sh icp network stop ``` -> `icp deploy --cycles 30t` is required because `caller` dynamically creates `callee` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `make topup`. +> `icp deploy --cycles 30t` is required because `caller` dynamically creates `callee` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `icp canister top-up --amount 30t caller`. Note that the first call to `put` is slow, since all five callee partitions are created at that point. diff --git a/rust/composite_query/callee/lib.rs b/rust/composite_query/callee/lib.rs index aeec77db0f..d994114ea9 100644 --- a/rust/composite_query/callee/lib.rs +++ b/rust/composite_query/callee/lib.rs @@ -22,3 +22,5 @@ fn get(key: u128) -> Option { r }) } + +ic_cdk::export_candid!(); diff --git a/rust/composite_query/caller/lib.rs b/rust/composite_query/caller/lib.rs index 867c498c1c..8554698843 100644 --- a/rust/composite_query/caller/lib.rs +++ b/rust/composite_query/caller/lib.rs @@ -98,6 +98,8 @@ fn lookup(key: u128) -> (u128, String) { ) } +ic_cdk::export_candid!(); + async fn create_partition_canister() { const T: u128 = 1_000_000_000_000; diff --git a/rust/composite_query/icp.yaml b/rust/composite_query/icp.yaml index 09d4a7530c..e4b7045a54 100644 --- a/rust/composite_query/icp.yaml +++ b/rust/composite_query/icp.yaml @@ -1,15 +1,21 @@ -networks: - - name: local - mode: managed - canisters: + # Only caller is deployed. callee is built as a dependency of caller: + # caller embeds the callee WASM at compile time via include_bytes!, so callee + # must be compiled first. The build steps below make this explicit. - name: caller - recipe: - type: "@dfinity/rust@v3.3.0" - configuration: - package: caller - - name: callee - recipe: - type: "@dfinity/rust@v3.3.0" - configuration: - package: callee + build: + steps: + - type: script + commands: + - command -v ic-wasm >/dev/null 2>&1 || { echo >&2 'ic-wasm not found. To install ic-wasm, see https://github.com/dfinity/ic-wasm'; exit 1; } + - command -v candid-extractor >/dev/null 2>&1 || { echo >&2 'candid-extractor not found. Run `cargo install candid-extractor` to install it.'; exit 1; } + - type: script + commands: + - cargo build --package callee --target wasm32-unknown-unknown --release + - cargo build --package caller --target wasm32-unknown-unknown --release + - TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p'); cp "${TARGET_DIR}/wasm32-unknown-unknown/release/caller.wasm" "$ICP_WASM_OUTPUT_PATH" + - type: script + commands: + - ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "cargo:version" -d "$(cargo --version)" --keep-name-section + - ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "template:type" -d "rust" --keep-name-section + - did="$(mktemp)"; candid-extractor "$ICP_WASM_OUTPUT_PATH" >"$did" && ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "candid:service" -f "$did" -v public --keep-name-section From 334bbbbc77243fa7d0687a8997adff82713f4d42 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:00:44 +0200 Subject: [PATCH 04/15] refactor(rust/composite_query): rename caller/callee to backend/bucket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with the Motoko example (Map/Bucket) and icp-cli conventions: - caller/ -> backend/ (package: backend) — the Map canister, deployed via icp.yaml - callee/ -> bucket/ (package: bucket) — the Bucket canister, built as a compile-time dependency embedded via include_bytes! in backend Also fixes pre-existing include_bytes! path bug: ../../target/ was pointing one level too high (to rust/target/ instead of rust/composite_query/target/). Corrected to ../target/. Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/Cargo.toml | 2 +- rust/composite_query/README.md | 26 +++++++++---------- .../{caller => backend}/Cargo.toml | 2 +- .../{caller => backend}/lib.rs | 7 ++--- .../{callee => bucket}/Cargo.toml | 2 +- .../composite_query/{callee => bucket}/lib.rs | 0 rust/composite_query/icp.yaml | 14 +++++----- rust/composite_query/test.sh | 26 +++++++++---------- 8 files changed, 40 insertions(+), 39 deletions(-) rename rust/composite_query/{caller => backend}/Cargo.toml (91%) rename rust/composite_query/{caller => backend}/lib.rs (92%) rename rust/composite_query/{callee => bucket}/Cargo.toml (91%) rename rust/composite_query/{callee => bucket}/lib.rs (100%) diff --git a/rust/composite_query/Cargo.toml b/rust/composite_query/Cargo.toml index 10b4a1ad9f..b44be39155 100644 --- a/rust/composite_query/Cargo.toml +++ b/rust/composite_query/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["caller", "callee"] +members = ["backend", "bucket"] resolver = "2" diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index 7eb7952561..94b03bc196 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -4,27 +4,27 @@ On the Internet Computer, regular query functions are fast (no consensus) but ha For more background see [Composite queries](https://docs.internetcomputer.org/guides/canister-calls/parallel-inter-canister-calls/#composite-queries) in the ICP developer docs. -This example implements a distributed key-value store (`caller`) that shards its entries across five dynamically-installed `callee` child canisters. Looking up a key requires calling the appropriate callee: +This example implements a distributed key-value store (`backend`) that shards its entries across five dynamically-installed `Bucket` child canisters. Looking up a key requires calling the appropriate bucket: -- `get(k)` — **composite query**: delegates to the correct `callee.get(k)` as a cross-canister query call. Fast, no consensus. -- `get_update(k)` — **update call**: same lookup, but via an update call to the callee. Slower (goes through consensus) but provided here for comparison. +- `get(k)` — **composite query**: delegates to the correct `Bucket.get(k)` as a cross-canister query call. Fast, no consensus. +- `get_update(k)` — **update call**: same lookup, but via an update call to the bucket. Slower (goes through consensus) but provided here for comparison. Both functions return the same result; the difference is latency and call semantics. ## Architecture ``` -caller - n = 5 callees ┌── callee 0 (keys 0, 5, 10, …) - key % n routes ────┼── callee 1 (keys 1, 6, 11, …) - ├── callee 2 (keys 2, 7, 12, …) - ├── callee 3 (keys 3, 8, 13, …) - └── callee 4 (keys 4, 9, 14, …) +backend (Map) + n = 5 buckets ┌── Bucket 0 (keys 0, 5, 10, …) + key % n routes ────┼── Bucket 1 (keys 1, 6, 11, …) + ├── Bucket 2 (keys 2, 7, 12, …) + ├── Bucket 3 (keys 3, 8, 13, …) + └── Bucket 4 (keys 4, 9, 14, …) ``` -`caller.put(k, v)` dynamically installs a `callee` canister if one does not exist for `k % 5`, then stores the entry there via an update call. `caller.get(k)` and `caller.get_update(k)` both route to the same callee via `k % 5`. +`backend.put(k, v)` dynamically installs a `Bucket` canister if one does not exist for `k % 5`, then stores the entry there. `backend.get(k)` and `backend.get_update(k)` both route to the same bucket via `k % 5`. -The `callee` WASM is embedded directly into the `caller` WASM binary at compile time via `include_bytes!`. This means only the `caller` canister needs to be deployed — it installs the `callee` canisters programmatically on first use. +The `Bucket` WASM is embedded directly into the `backend` WASM binary at compile time via `include_bytes!`. Only the `backend` canister is deployed — it installs `Bucket` canisters programmatically on the first `put` call. ## Build and deploy from the command line @@ -49,9 +49,9 @@ bash test.sh icp network stop ``` -> `icp deploy --cycles 30t` is required because `caller` dynamically creates `callee` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `icp canister top-up --amount 30t caller`. +> `icp deploy --cycles 30t` is required because `backend` dynamically creates `Bucket` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `icp canister top-up --amount 30t backend`. -Note that the first call to `put` is slow, since all five callee partitions are created at that point. +Note that the first call to `put` is slow, since all five `Bucket` partitions are created at that point. ## Security considerations and best practices diff --git a/rust/composite_query/caller/Cargo.toml b/rust/composite_query/backend/Cargo.toml similarity index 91% rename from rust/composite_query/caller/Cargo.toml rename to rust/composite_query/backend/Cargo.toml index 9b999d1f15..2f87a29d78 100644 --- a/rust/composite_query/caller/Cargo.toml +++ b/rust/composite_query/backend/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "caller" +name = "backend" version = "0.1.0" edition = "2021" diff --git a/rust/composite_query/caller/lib.rs b/rust/composite_query/backend/lib.rs similarity index 92% rename from rust/composite_query/caller/lib.rs rename to rust/composite_query/backend/lib.rs index 8554698843..105cc23b29 100644 --- a/rust/composite_query/caller/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -12,9 +12,10 @@ use std::sync::RwLock; const NUM_PARTITIONS: usize = 5; -// Inline wasm binary of the callee canister +// Bucket WASM compiled separately and embedded at compile time via include_bytes!; +// the backend installs it into new canister instances at runtime on first put(). pub const WASM: &[u8] = - include_bytes!("../../target/wasm32-unknown-unknown/release/callee.wasm"); + include_bytes!("../target/wasm32-unknown-unknown/release/bucket.wasm"); thread_local! { // A list of canister IDs for data partitions @@ -105,7 +106,7 @@ async fn create_partition_canister() { let create_args = CreateCanisterArgs { settings: Some(CanisterSettings { - controllers: Some(vec![ic_cdk::canister_self()]), + controllers: Some(vec![ic_cdk::api::canister_self()]), compute_allocation: None, memory_allocation: None, freezing_threshold: None, diff --git a/rust/composite_query/callee/Cargo.toml b/rust/composite_query/bucket/Cargo.toml similarity index 91% rename from rust/composite_query/callee/Cargo.toml rename to rust/composite_query/bucket/Cargo.toml index cce59b7e97..adab198a78 100644 --- a/rust/composite_query/callee/Cargo.toml +++ b/rust/composite_query/bucket/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "callee" +name = "bucket" version = "0.1.0" edition = "2021" diff --git a/rust/composite_query/callee/lib.rs b/rust/composite_query/bucket/lib.rs similarity index 100% rename from rust/composite_query/callee/lib.rs rename to rust/composite_query/bucket/lib.rs diff --git a/rust/composite_query/icp.yaml b/rust/composite_query/icp.yaml index e4b7045a54..d97c56ecb4 100644 --- a/rust/composite_query/icp.yaml +++ b/rust/composite_query/icp.yaml @@ -1,8 +1,8 @@ canisters: - # Only caller is deployed. callee is built as a dependency of caller: - # caller embeds the callee WASM at compile time via include_bytes!, so callee - # must be compiled first. The build steps below make this explicit. - - name: caller + # Only backend (Map) is deployed. bucket (Bucket) is a compile-time dependency: + # backend embeds the bucket WASM via include_bytes! and installs bucket canister + # instances programmatically at runtime on the first put() call. + - name: backend build: steps: - type: script @@ -11,9 +11,9 @@ canisters: - command -v candid-extractor >/dev/null 2>&1 || { echo >&2 'candid-extractor not found. Run `cargo install candid-extractor` to install it.'; exit 1; } - type: script commands: - - cargo build --package callee --target wasm32-unknown-unknown --release - - cargo build --package caller --target wasm32-unknown-unknown --release - - TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p'); cp "${TARGET_DIR}/wasm32-unknown-unknown/release/caller.wasm" "$ICP_WASM_OUTPUT_PATH" + - cargo build --package bucket --target wasm32-unknown-unknown --release + - cargo build --package backend --target wasm32-unknown-unknown --release + - cp "target/wasm32-unknown-unknown/release/backend.wasm" "$ICP_WASM_OUTPUT_PATH" - type: script commands: - ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "cargo:version" -d "$(cargo --version)" --keep-name-section diff --git a/rust/composite_query/test.sh b/rust/composite_query/test.sh index 2147242f3d..064b8b8562 100755 --- a/rust/composite_query/test.sh +++ b/rust/composite_query/test.sh @@ -2,42 +2,42 @@ set -e echo "=== Test 1: put inserts a key-value pair (also creates callee partitions) ===" -result=$(icp canister call caller put '(1 : nat, 1337 : nat)') && \ +result=$(icp canister call backend put '(1 : nat, 1337 : nat)') && \ echo "$result" && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 2: put a second value for the same key returns the old value ===" -result=$(icp canister call caller put '(1 : nat, 42 : nat)') && \ +result=$(icp canister call backend put '(1 : nat, 42 : nat)') && \ echo "$result" && \ echo "$result" | grep -q '1_337' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 3: get (composite query) retrieves the stored value ===" -result=$(icp canister call --query caller get '(1 : nat)') && \ +result=$(icp canister call --query backend get '(1 : nat)') && \ echo "$result" && \ echo "$result" | grep -q 'opt (42' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 4: get_update returns the same value via an update call ===" -result=$(icp canister call caller get_update '(1 : nat)') && \ +result=$(icp canister call backend get_update '(1 : nat)') && \ echo "$result" && \ echo "$result" | grep -q 'opt (42' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 5: get returns null for a missing key ===" -result=$(icp canister call --query caller get '(99 : nat)') && \ +result=$(icp canister call --query backend get '(99 : nat)') && \ echo "$result" && \ echo "$result" | grep -q 'null' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 6: composite query routes correctly across partitions ===" -icp canister call caller put '(2 : nat, 100 : nat)' && \ - icp canister call caller put '(3 : nat, 200 : nat)' && \ - icp canister call caller put '(4 : nat, 300 : nat)' && \ - result1=$(icp canister call --query caller get '(1 : nat)') && \ - result2=$(icp canister call --query caller get '(2 : nat)') && \ - result3=$(icp canister call --query caller get '(3 : nat)') && \ - result4=$(icp canister call --query caller get '(4 : nat)') && \ +icp canister call backend put '(2 : nat, 100 : nat)' && \ + icp canister call backend put '(3 : nat, 200 : nat)' && \ + icp canister call backend put '(4 : nat, 300 : nat)' && \ + result1=$(icp canister call --query backend get '(1 : nat)') && \ + result2=$(icp canister call --query backend get '(2 : nat)') && \ + result3=$(icp canister call --query backend get '(3 : nat)') && \ + result4=$(icp canister call --query backend get '(4 : nat)') && \ echo "$result1" && echo "$result2" && echo "$result3" && echo "$result4" && \ echo "$result1" | grep -q 'opt (42' && \ echo "$result2" | grep -q 'opt (100' && \ @@ -46,7 +46,7 @@ icp canister call caller put '(2 : nat, 100 : nat)' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 7: lookup (query) returns the partition index and canister ID ===" -result=$(icp canister call --query caller lookup '(1 : nat)') && \ +result=$(icp canister call --query backend lookup '(1 : nat)') && \ echo "$result" && \ echo "$result" | grep -q '1 :' && \ echo "PASS" || (echo "FAIL" && exit 1) From 07815313f0b3e1b3d47619f8ebac9b1f0ccfd8ff Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:05:12 +0200 Subject: [PATCH 05/15] fix(rust/composite_query): allocate dynamic cycle share per bucket like Motoko Fixed InsufficientLiquidCycleBalance in CI: hardcoded 10T per bucket exceeded the available balance. Now divides evenly like the Motoko example: cycleShare = Cycles.balance() / (NUM_PARTITIONS + 1) Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/backend/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rust/composite_query/backend/lib.rs b/rust/composite_query/backend/lib.rs index 105cc23b29..ea6c754435 100644 --- a/rust/composite_query/backend/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -102,7 +102,10 @@ fn lookup(key: u128) -> (u128, String) { ic_cdk::export_candid!(); async fn create_partition_canister() { - const T: u128 = 1_000_000_000_000; + // Divide the available balance equally among all buckets plus the backend itself, + // mirroring the Motoko approach: cycleShare = Cycles.balance() / (n + 1). + let cycle_share = + ic_cdk::api::canister_cycle_balance() / (NUM_PARTITIONS as u128 + 1); let create_args = CreateCanisterArgs { settings: Some(CanisterSettings { @@ -119,12 +122,12 @@ async fn create_partition_canister() { }), }; - let result = create_canister_with_extra_cycles(&create_args, 10 * T) + let result = create_canister_with_extra_cycles(&create_args, cycle_share) .await .unwrap(); let canister_id = result.canister_id; - ic_cdk::println!("Created callee canister {}", canister_id); + ic_cdk::println!("Created bucket canister {}", canister_id); let install_args = InstallCodeArgs { mode: CanisterInstallMode::Install, From 4cf2b4decebfe1bfccb599e5339dbf9995b22ce1 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:08:38 +0200 Subject: [PATCH 06/15] docs(rust/composite_query): fix README explanation of cycle allocation Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index 94b03bc196..5311b0e268 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -24,7 +24,7 @@ backend (Map) `backend.put(k, v)` dynamically installs a `Bucket` canister if one does not exist for `k % 5`, then stores the entry there. `backend.get(k)` and `backend.get_update(k)` both route to the same bucket via `k % 5`. -The `Bucket` WASM is embedded directly into the `backend` WASM binary at compile time via `include_bytes!`. Only the `backend` canister is deployed — it installs `Bucket` canisters programmatically on the first `put` call. +The `Bucket` WASM is embedded directly into the `backend` WASM binary at compile time via `include_bytes!`. Only the `backend` canister is deployed — it installs `Bucket` canisters programmatically on the first `put` call. The backend divides its available cycle balance equally among the buckets and itself, mirroring the Motoko approach. ## Build and deploy from the command line From 146db1b13c539424a6fe274f56d3e874839f063a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:24:04 +0200 Subject: [PATCH 07/15] fix(rust/composite_query): use candid_tuple for method response decoding ic-cdk 0.20's candid::() uses decode_one (single value) but a canister method's return is encoded as a Candid argument list, which requires decode_args (used by candid_tuple). Using candid::() caused all inter-canister call responses to decode silently as None, making test 2 fail (put returned null instead of the previous value). Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/backend/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/composite_query/backend/lib.rs b/rust/composite_query/backend/lib.rs index ea6c754435..dcd09b9773 100644 --- a/rust/composite_query/backend/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -42,7 +42,7 @@ async fn put(key: u128, value: u128) -> Option { .with_args(&(key, value)) .await .ok() - .and_then(|r| r.candid::<(Option,)>().ok()) + .and_then(|r| r.candid_tuple::<(Option,)>().ok()) .and_then(|(v,)| v) } @@ -59,7 +59,7 @@ async fn get(key: u128) -> Option { .with_arg(key) .await .ok() - .and_then(|r| r.candid::<(Option,)>().ok()) + .and_then(|r| r.candid_tuple::<(Option,)>().ok()) .and_then(|(v,)| v) } @@ -76,7 +76,7 @@ async fn get_update(key: u128) -> Option { .with_arg(key) .await .ok() - .and_then(|r| r.candid::<(Option,)>().ok()) + .and_then(|r| r.candid_tuple::<(Option,)>().ok()) .and_then(|(v,)| v) } From 60d286b73d4bffa6381ab950b45c68f74c0c795f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:26:44 +0200 Subject: [PATCH 08/15] fix(rust/composite_query): rename callee to Bucket in test description Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/composite_query/test.sh b/rust/composite_query/test.sh index 064b8b8562..dd5bb71cbf 100755 --- a/rust/composite_query/test.sh +++ b/rust/composite_query/test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -echo "=== Test 1: put inserts a key-value pair (also creates callee partitions) ===" +echo "=== Test 1: put inserts a key-value pair (also creates Bucket partitions) ===" result=$(icp canister call backend put '(1 : nat, 1337 : nat)') && \ echo "$result" && \ echo "PASS" || (echo "FAIL" && exit 1) From 6299e27d3acfc41e2384b2b257aad16a2e852994 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:28:13 +0200 Subject: [PATCH 09/15] fix(rust/composite_query): update stale caller/callee references in log messages Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/backend/lib.rs | 6 +++--- rust/composite_query/bucket/lib.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/composite_query/backend/lib.rs b/rust/composite_query/backend/lib.rs index dcd09b9773..99c99db4ac 100644 --- a/rust/composite_query/backend/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -33,7 +33,7 @@ async fn put(key: u128, value: u128) -> Option { let canister_id = get_partition_for_key(key); ic_cdk::println!( - "Put in caller for key={} .. using callee={}", + "Put in backend for key={} .. using bucket={}", key, canister_id.to_text() ); @@ -50,7 +50,7 @@ async fn put(key: u128, value: u128) -> Option { async fn get(key: u128) -> Option { let canister_id = get_partition_for_key(key); ic_cdk::println!( - "Get in caller for key={} .. using callee={}", + "Get in backend for key={} .. using bucket={}", key, canister_id.to_text() ); @@ -67,7 +67,7 @@ async fn get(key: u128) -> Option { async fn get_update(key: u128) -> Option { let canister_id = get_partition_for_key(key); ic_cdk::println!( - "Get as update in caller for key={} .. using callee={}", + "Get as update in backend for key={} .. using bucket={}", key, canister_id.to_text() ); diff --git a/rust/composite_query/bucket/lib.rs b/rust/composite_query/bucket/lib.rs index d994114ea9..1c1e9316db 100644 --- a/rust/composite_query/bucket/lib.rs +++ b/rust/composite_query/bucket/lib.rs @@ -10,7 +10,7 @@ thread_local! { #[update] fn put(key: u128, value: u128) -> Option { - ic_cdk::println!("Set in callee for key={} with value={}", key, value); + ic_cdk::println!("Set in bucket for key={} with value={}", key, value); STORE.with(|store| store.borrow_mut().insert(key, value)) } @@ -18,7 +18,7 @@ fn put(key: u128, value: u128) -> Option { fn get(key: u128) -> Option { STORE.with(|store| { let r = store.borrow().get(&key); - ic_cdk::println!("Get in callee for key={} - result={:?}", key, r); + ic_cdk::println!("Get in bucket for key={} - result={:?}", key, r); r }) } From 4c47dbd735589b7532e2865ca7ba1e69eb39252b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 10:53:39 +0200 Subject: [PATCH 10/15] fix(rust/composite_query): bounds-safe routing, handle uninitialized state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_partition_for_key returns Option using .get() instead of direct indexing — safe for empty or partially-initialized CANISTER_IDS - put/get/get_update return None early if the bucket for the key is not yet available (empty state or partial init from concurrent put) - lookup uses .get().map().unwrap_or_default() instead of direct indexing Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/backend/lib.rs | 39 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/rust/composite_query/backend/lib.rs b/rust/composite_query/backend/lib.rs index 99c99db4ac..34311a9e14 100644 --- a/rust/composite_query/backend/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -31,7 +31,12 @@ async fn put(key: u128, value: u128) -> Option { } } - let canister_id = get_partition_for_key(key); + // If initialization is still in progress (another concurrent put interleaved), + // the bucket for this key may not exist yet — return None rather than trap. + let canister_id = match get_partition_for_key(key) { + Some(id) => id, + None => return None, + }; ic_cdk::println!( "Put in backend for key={} .. using bucket={}", key, @@ -48,7 +53,10 @@ async fn put(key: u128, value: u128) -> Option { #[query(composite = true)] async fn get(key: u128) -> Option { - let canister_id = get_partition_for_key(key); + let canister_id = match get_partition_for_key(key) { + Some(id) => id, + None => return None, + }; ic_cdk::println!( "Get in backend for key={} .. using bucket={}", key, @@ -65,7 +73,10 @@ async fn get(key: u128) -> Option { #[update] async fn get_update(key: u128) -> Option { - let canister_id = get_partition_for_key(key); + let canister_id = match get_partition_for_key(key) { + Some(id) => id, + None => return None, + }; ic_cdk::println!( "Get as update in backend for key={} .. using bucket={}", key, @@ -80,23 +91,25 @@ async fn get_update(key: u128) -> Option { .and_then(|(v,)| v) } -fn get_partition_for_key(key: u128) -> Principal { +fn get_partition_for_key(key: u128) -> Option { + let idx = (key % NUM_PARTITIONS as u128) as usize; CANISTER_IDS.with(|canister_ids| { - let canister_ids = canister_ids.read().unwrap(); - canister_ids[lookup(key).0 as usize] + canister_ids.read().unwrap().get(idx).copied() }) } #[query(composite = true)] fn lookup(key: u128) -> (u128, String) { let r = key % NUM_PARTITIONS as u128; - ( - r, - CANISTER_IDS.with(|canister_ids| { - let canister_ids = canister_ids.read().unwrap(); - canister_ids[r as usize].to_text() - }), - ) + let canister_text = CANISTER_IDS.with(|canister_ids| { + canister_ids + .read() + .unwrap() + .get(r as usize) + .map(|id| id.to_text()) + .unwrap_or_default() + }); + (r, canister_text) } ic_cdk::export_candid!(); From 92396c8b3620aaa76dfe03c59bb898982af47ec6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 11:07:10 +0200 Subject: [PATCH 11/15] fix(rust/composite_query): address Copilot review comments - Compute cycle_share once before the loop so all buckets receive the same amount (fixes the decreasing-allocation bug) - Update ic-wasm install hint to use npm instead of GitHub repo link - Test 1: assert return value is null (new key has no previous value) - Test 7: assert canister ID is non-empty (not just that index 1 appears) Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/backend/lib.rs | 12 ++++++------ rust/composite_query/icp.yaml | 2 +- rust/composite_query/test.sh | 4 +++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/rust/composite_query/backend/lib.rs b/rust/composite_query/backend/lib.rs index 34311a9e14..a11871f955 100644 --- a/rust/composite_query/backend/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -26,8 +26,12 @@ thread_local! { async fn put(key: u128, value: u128) -> Option { // Create partitions if they don't exist yet if CANISTER_IDS.with(|canister_ids| canister_ids.read().unwrap().is_empty()) { + // Compute the share once so every bucket receives the same amount, + // mirroring the Motoko approach: cycleShare = Cycles.balance() / (n + 1). + let cycle_share = + ic_cdk::api::canister_cycle_balance() / (NUM_PARTITIONS as u128 + 1); for _ in 0..NUM_PARTITIONS { - create_partition_canister().await; + create_partition_canister(cycle_share).await; } } @@ -114,11 +118,7 @@ fn lookup(key: u128) -> (u128, String) { ic_cdk::export_candid!(); -async fn create_partition_canister() { - // Divide the available balance equally among all buckets plus the backend itself, - // mirroring the Motoko approach: cycleShare = Cycles.balance() / (n + 1). - let cycle_share = - ic_cdk::api::canister_cycle_balance() / (NUM_PARTITIONS as u128 + 1); +async fn create_partition_canister(cycle_share: u128) { let create_args = CreateCanisterArgs { settings: Some(CanisterSettings { diff --git a/rust/composite_query/icp.yaml b/rust/composite_query/icp.yaml index d97c56ecb4..680c15ff8a 100644 --- a/rust/composite_query/icp.yaml +++ b/rust/composite_query/icp.yaml @@ -7,7 +7,7 @@ canisters: steps: - type: script commands: - - command -v ic-wasm >/dev/null 2>&1 || { echo >&2 'ic-wasm not found. To install ic-wasm, see https://github.com/dfinity/ic-wasm'; exit 1; } + - command -v ic-wasm >/dev/null 2>&1 || { echo >&2 'ic-wasm not found. Run `npm install -g @icp-sdk/ic-wasm` to install it.'; exit 1; } - command -v candid-extractor >/dev/null 2>&1 || { echo >&2 'candid-extractor not found. Run `cargo install candid-extractor` to install it.'; exit 1; } - type: script commands: diff --git a/rust/composite_query/test.sh b/rust/composite_query/test.sh index dd5bb71cbf..130643020e 100755 --- a/rust/composite_query/test.sh +++ b/rust/composite_query/test.sh @@ -4,6 +4,7 @@ set -e echo "=== Test 1: put inserts a key-value pair (also creates Bucket partitions) ===" result=$(icp canister call backend put '(1 : nat, 1337 : nat)') && \ echo "$result" && \ + echo "$result" | grep -q 'null' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 2: put a second value for the same key returns the old value ===" @@ -45,8 +46,9 @@ icp canister call backend put '(2 : nat, 100 : nat)' && \ echo "$result4" | grep -q 'opt (300' && \ echo "PASS" || (echo "FAIL" && exit 1) -echo "=== Test 7: lookup (query) returns the partition index and canister ID ===" +echo "=== Test 7: lookup (query) returns the partition index and a non-empty canister ID ===" result=$(icp canister call --query backend lookup '(1 : nat)') && \ echo "$result" && \ echo "$result" | grep -q '1 :' && \ + echo "$result" | grep -qE '[a-z0-9]{5}-[a-z0-9]{5}' && \ echo "PASS" || (echo "FAIL" && exit 1) From 9f7df46059fe03a46d2771398980e24173dc27e5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 11:13:01 +0200 Subject: [PATCH 12/15] fix(rust/composite_query): make test 1 idempotent across re-runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the null assertion from test 1 — the return value is the previous value for key 1 (null on first run, a nat on subsequent runs). Tests 2-7 are already idempotent since they overwrite the same keys with the same values each time. Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/composite_query/test.sh b/rust/composite_query/test.sh index 130643020e..a4fae2606e 100755 --- a/rust/composite_query/test.sh +++ b/rust/composite_query/test.sh @@ -2,9 +2,9 @@ set -e echo "=== Test 1: put inserts a key-value pair (also creates Bucket partitions) ===" +# The return value is the previous value for key 1 (null on first run, a nat on subsequent runs). result=$(icp canister call backend put '(1 : nat, 1337 : nat)') && \ echo "$result" && \ - echo "$result" | grep -q 'null' && \ echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 2: put a second value for the same key returns the old value ===" From 8c2e891a0a0819c0d705c4b09bdb82d04ac80ab6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 11:15:19 +0200 Subject: [PATCH 13/15] docs(rust/composite_query): note that test.sh is idempotent across re-runs Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index 5311b0e268..72c3be9b22 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -51,7 +51,7 @@ icp network stop > `icp deploy --cycles 30t` is required because `backend` dynamically creates `Bucket` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `icp canister top-up --amount 30t backend`. -Note that the first call to `put` is slow, since all five `Bucket` partitions are created at that point. +Note that the first call to `put` is slow, since all five `Bucket` partitions are created at that point. `bash test.sh` can be re-run on the same deployment — tests 2–7 overwrite the same keys with the same values and are idempotent. ## Security considerations and best practices From 1e8e3d60b8c52aef91ca78ac8589a4b35bc72235 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 11:25:28 +0200 Subject: [PATCH 14/15] fix(rust/composite_query): address Copilot review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change lookup from #[query(composite = true)] to plain #[query] — it makes no inter-canister calls so composite = true is misleading; the composite query showcase is fully covered by get() - Fix README: backend creates all 5 Bucket canisters on the first put call (not lazily per key as the old wording implied) - Fix stale 'data partitions' comment -> 'Bucket canisters' Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/README.md | 2 +- rust/composite_query/backend/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/composite_query/README.md b/rust/composite_query/README.md index 72c3be9b22..6adbc1645b 100644 --- a/rust/composite_query/README.md +++ b/rust/composite_query/README.md @@ -22,7 +22,7 @@ backend (Map) └── Bucket 4 (keys 4, 9, 14, …) ``` -`backend.put(k, v)` dynamically installs a `Bucket` canister if one does not exist for `k % 5`, then stores the entry there. `backend.get(k)` and `backend.get_update(k)` both route to the same bucket via `k % 5`. +`backend.put(k, v)` creates all five `Bucket` canisters on the first call, then stores the entry in the one responsible for `k % 5`. `backend.get(k)` and `backend.get_update(k)` both route to the same bucket via `k % 5`. The `Bucket` WASM is embedded directly into the `backend` WASM binary at compile time via `include_bytes!`. Only the `backend` canister is deployed — it installs `Bucket` canisters programmatically on the first `put` call. The backend divides its available cycle balance equally among the buckets and itself, mirroring the Motoko approach. diff --git a/rust/composite_query/backend/lib.rs b/rust/composite_query/backend/lib.rs index a11871f955..a6b7c27685 100644 --- a/rust/composite_query/backend/lib.rs +++ b/rust/composite_query/backend/lib.rs @@ -18,7 +18,7 @@ pub const WASM: &[u8] = include_bytes!("../target/wasm32-unknown-unknown/release/bucket.wasm"); thread_local! { - // A list of canister IDs for data partitions + // A list of canister IDs for the Bucket canisters static CANISTER_IDS: Arc>> = Arc::new(RwLock::new(vec![])); } @@ -102,7 +102,7 @@ fn get_partition_for_key(key: u128) -> Option { }) } -#[query(composite = true)] +#[query] fn lookup(key: u128) -> (u128, String) { let r = key % NUM_PARTITIONS as u128; let canister_text = CANISTER_IDS.with(|canister_ids| { From 8590b0b4afa1f94ae658ed62402777a3f40398e5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 19 Jun 2026 11:45:37 +0200 Subject: [PATCH 15/15] fix(rust/composite_query): remove candid-extractor from build steps candid-extractor is not part of the standard icp-cli prerequisites (npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm). The custom build steps are only needed to compile bucket before backend; dropping the Candid interface extraction step removes the extra dependency. The canister is fully functional without the candid:service WASM metadata. Co-Authored-By: Claude Sonnet 4.6 --- rust/composite_query/icp.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/rust/composite_query/icp.yaml b/rust/composite_query/icp.yaml index 680c15ff8a..fbef060708 100644 --- a/rust/composite_query/icp.yaml +++ b/rust/composite_query/icp.yaml @@ -8,7 +8,6 @@ canisters: - type: script commands: - command -v ic-wasm >/dev/null 2>&1 || { echo >&2 'ic-wasm not found. Run `npm install -g @icp-sdk/ic-wasm` to install it.'; exit 1; } - - command -v candid-extractor >/dev/null 2>&1 || { echo >&2 'candid-extractor not found. Run `cargo install candid-extractor` to install it.'; exit 1; } - type: script commands: - cargo build --package bucket --target wasm32-unknown-unknown --release @@ -18,4 +17,3 @@ canisters: commands: - ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "cargo:version" -d "$(cargo --version)" --keep-name-section - ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "template:type" -d "rust" --keep-name-section - - did="$(mktemp)"; candid-extractor "$ICP_WASM_OUTPUT_PATH" >"$did" && ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "${ICP_WASM_OUTPUT_PATH}" metadata "candid:service" -f "$did" -v public --keep-name-section