diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 70c9ba04bda..2d56299ab9e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,7 +37,7 @@ updates: target-branch: "main" directories: - / - - /wasm_samples + - /wasm schedule: interval: "daily" commit-message: diff --git a/.github/workflows/iroha2-custom-image.yml b/.github/workflows/iroha2-custom-image.yml index 2ebdb726114..deacd0d5e95 100644 --- a/.github/workflows/iroha2-custom-image.yml +++ b/.github/workflows/iroha2-custom-image.yml @@ -37,10 +37,11 @@ env: IROHA2_RUSTFLAGS: -C force-frame-pointers=on IROHA2_FEATURES: profiling IROHA2_CARGOFLAGS: -Z build-std - DOCKER_COMPOSE_PATH: defaults + DEFAULTS_DIR: defaults + WASM_TARGET_DIR: wasm/target/prebuilt jobs: - build_executor: + build_wasm_libs: runs-on: ubuntu-latest container: image: hyperledger/iroha2-ci:nightly-2024-09-09 @@ -49,30 +50,32 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.CHECKOUT_REF }} - - name: Build iroha executor - run: mold --run cargo run --bin iroha_wasm_builder -- build ./wasm_samples/default_executor --optimize --out-file ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm - - name: Upload executor to reuse in other jobs + - name: Build wasm libs + run: ./scripts/build_wasm.sh libs + - name: Upload wasm libs to reuse in other jobs uses: actions/upload-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + name: wasm-libs + path: ${{ env.WASM_TARGET_DIR }}/libs retention-days: 1 registry-profiling-image: if: ${{ inputs.BUILD_GLIBC_IMAGE == 'false' }} and ${{ inputs.BUILD_ALPINE_IMAGE == 'false' }} runs-on: [self-hosted, Linux, iroha2] - needs: build_executor + needs: build_wasm_libs container: image: hyperledger/iroha2-ci:nightly-2024-09-09 steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.CHECKOUT_REF }} - - name: Download executor.wasm file + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm-libs + path: ${{ env.DEFAULTS_DIR }}/libs + - name: Move the default executor + run: mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Get the release tag run: | RELEASE_VERSION=${{ github.ref_name }} @@ -115,18 +118,20 @@ jobs: registry-glibc-image: if: ${{ inputs.BUILD_GLIBC_IMAGE == 'true' }} and ${{ inputs.BUILD_ALPINE_IMAGE == 'false' }} runs-on: [self-hosted, Linux, iroha2] - needs: build_executor + needs: build_wasm_libs container: image: hyperledger/iroha2-ci:nightly-2024-09-09 steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.CHECKOUT_REF }} - - name: Download executor.wasm file + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm-libs + path: ${{ env.DEFAULTS_DIR }}/libs + - name: Move the default executor + run: mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Login to Soramitsu Harbor uses: docker/login-action@v3 with: @@ -152,18 +157,20 @@ jobs: registry-alpine-image: if: ${{ inputs.BUILD_GLIBC_IMAGE == 'false' }} and ${{ inputs.BUILD_ALPINE_IMAGE == 'true' }} runs-on: [self-hosted, Linux, iroha2] - needs: build_executor + needs: build_wasm_libs container: image: hyperledger/iroha2-ci:nightly-2024-09-09 steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.CHECKOUT_REF }} - - name: Download executor.wasm file + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm-libs + path: ${{ env.DEFAULTS_DIR }}/libs + - name: Move the default executor + run: mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Login to Soramitsu Harbor uses: docker/login-action@v3 with: diff --git a/.github/workflows/iroha2-dev-nightly.yml b/.github/workflows/iroha2-dev-nightly.yml index c3fec1acf93..1b7b5d6237e 100644 --- a/.github/workflows/iroha2-dev-nightly.yml +++ b/.github/workflows/iroha2-dev-nightly.yml @@ -3,37 +3,40 @@ name: I2::Dev::Nightly::Publish on: workflow_dispatch env: - DOCKER_COMPOSE_PATH: defaults + DEFAULTS_DIR: defaults + WASM_TARGET_DIR: wasm/target/prebuilt jobs: - build_executor: + build_wasm_libs: runs-on: ubuntu-latest container: image: hyperledger/iroha2-ci:nightly-2024-09-09 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - name: Build iroha executor - run: mold --run cargo run --bin iroha_wasm_builder -- build ./wasm_samples/default_executor --optimize --out-file ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm - - name: Upload executor to reuse in other jobs + - name: Build wasm libs + run: ./scripts/build_wasm.sh libs + - name: Upload wasm libs to reuse in other jobs uses: actions/upload-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + name: wasm-libs + path: ${{ env.WASM_TARGET_DIR }}/libs retention-days: 1 dockerhub: runs-on: ubuntu-latest - needs: build_executor + needs: build_wasm_libs container: image: hyperledger/iroha2-ci:nightly-2024-09-09 steps: - uses: actions/checkout@v4 - - name: Download executor.wasm file + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm-libs + path: ${{ env.DEFAULTS_DIR }}/libs + - name: Move the default executor + run: mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/iroha2-dev-pr-static.yml b/.github/workflows/iroha2-dev-pr-static.yml index 7afbfe23fbf..e276eebe678 100644 --- a/.github/workflows/iroha2-dev-pr-static.yml +++ b/.github/workflows/iroha2-dev-pr-static.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - DOCKER_COMPOSE_PATH: defaults + DEFAULTS_DIR: defaults jobs: smart_contracts_analysis: @@ -24,8 +24,8 @@ jobs: image: hyperledger/iroha2-ci:nightly-2024-09-09 steps: - uses: actions/checkout@v4 - - name: cargo fmt (wasm_samples) - working-directory: wasm_samples + - name: cargo fmt (wasm) + working-directory: wasm run: cargo fmt --all -- --check python_static_analysis: diff --git a/.github/workflows/iroha2-dev-pr-wasm.yaml b/.github/workflows/iroha2-dev-pr-wasm.yml similarity index 88% rename from .github/workflows/iroha2-dev-pr-wasm.yaml rename to .github/workflows/iroha2-dev-pr-wasm.yml index 46ea3c64dc1..ab682d7f657 100644 --- a/.github/workflows/iroha2-dev-pr-wasm.yaml +++ b/.github/workflows/iroha2-dev-pr-wasm.yml @@ -36,7 +36,7 @@ concurrency: cancel-in-progress: true env: - DOCKER_COMPOSE_PATH: defaults + DEFAULTS_DIR: defaults jobs: build_executor: @@ -46,13 +46,13 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - name: Build iroha executor - run: mold --run cargo run --bin iroha_wasm_builder -- build ./wasm_samples/default_executor --optimize --out-file ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + - name: Build wasm libs + run: mold --run cargo run --bin iroha_wasm_builder -- build ./wasm/libs/default_executor --optimize --out-file ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Upload executor to reuse in other jobs uses: actions/upload-artifact@v4 with: name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + path: ${{ env.DEFAULTS_DIR }}/executor.wasm retention-days: 1 tests: @@ -66,7 +66,7 @@ jobs: uses: actions/download-artifact@v4 with: name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + path: ${{ env.DEFAULTS_DIR }} - name: Install iroha_wasm_test_runner run: which iroha_wasm_test_runner || cargo install --path crates/iroha_wasm_test_runner - name: Run smart contract tests on WebAssembly VM diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 28f76f1e25d..ff127fca46a 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -18,8 +18,8 @@ concurrency: env: CARGO_TERM_COLOR: always IROHA_CLI_DIR: "/__w/${{ github.event.repository.name }}/${{ github.event.repository.name }}/test" - DOCKER_COMPOSE_PATH: defaults - WASM_SAMPLES_TARGET_DIR: wasm_samples/target/prebuilt + DEFAULTS_DIR: defaults + WASM_TARGET_DIR: wasm/target/prebuilt TEST_NETWORK_TMP_DIR: /tmp NEXTEST_PROFILE: ci @@ -63,7 +63,7 @@ jobs: name: report-clippy path: clippy.json - build_wasm_samples: + build_wasm: runs-on: ubuntu-latest container: image: hyperledger/iroha2-ci:nightly-2024-09-09 @@ -71,41 +71,34 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build - run: ./scripts/build_wasm_samples.sh + run: ./scripts/build_wasm.sh - name: Upload all built WASMs uses: actions/upload-artifact@v4 with: - name: wasm_samples - path: ${{ env.WASM_SAMPLES_TARGET_DIR }} - retention-days: 1 - - name: Upload executor.wasm specifically - uses: actions/upload-artifact@v4 - with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + name: wasm + path: ${{ env.WASM_TARGET_DIR }} retention-days: 1 test_with_coverage: runs-on: [self-hosted, Linux, iroha2] container: image: hyperledger/iroha2-ci:nightly-2024-09-09 - needs: build_wasm_samples + needs: build_wasm env: LLVM_PROFILE_FILE_NAME: "iroha-%p-%m.profraw" steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@nextest - uses: taiki-e/install-action@cargo-llvm-cov - - name: Download executor.wasm + - name: Download wasm uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} - - name: Download the rest of WASM samples - uses: actions/download-artifact@v4 - with: - name: wasm_samples - path: ${{ env.WASM_SAMPLES_TARGET_DIR }} + name: wasm + path: ${{ env.WASM_TARGET_DIR }} + - name: Move wasm libs + run: | + mv ${{ env.WASM_TARGET_DIR }}/libs ${{ env.DEFAULTS_DIR }}/libs + mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Install irohad run: which irohad || cargo install --path crates/irohad --locked - name: Test with no default features @@ -169,7 +162,7 @@ jobs: context: . docker-compose-and-pytests: - needs: build_wasm_samples + needs: build_wasm runs-on: [self-hosted, Linux, iroha2] timeout-minutes: 60 env: @@ -181,11 +174,15 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Download executor.wasm + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm + path: ${{ env.WASM_TARGET_DIR }} + - name: Move wasm libs + run: | + mv ${{ env.WASM_TARGET_DIR }}/libs ${{ env.DEFAULTS_DIR }}/libs + mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Install Python and Poetry run: | yum install -y python${{ env.PYTHON_VERSION }} python${{ env.PYTHON_VERSION }}-devel @@ -211,14 +208,14 @@ jobs: cache-to: type=gha,mode=max - name: Test docker-compose.single.yml run: | - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.single.yml up --wait || exit 1 - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.single.yml down + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.single.yml up --wait || exit 1 + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.single.yml down - name: Test docker-compose.local.yml run: | - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.local.yml up --wait || exit 1 - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.local.yml down + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.local.yml up --wait || exit 1 + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.local.yml down - name: Run docker-compose.yml containers - run: docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.yml up --wait || exit 1 + run: docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.yml up --wait || exit 1 - name: Install Torii pytest dependencies working-directory: pytests/iroha_torii_tests run: ${{ env.POETRY_PATH }} install @@ -248,8 +245,8 @@ jobs: cd pytests/iroha_cli_tests ${{ env.POETRY_PATH }} run pytest on_retry_command: | - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.yml down - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.local.yml up --wait || exit 1 + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.yml down + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.local.yml up --wait || exit 1 - name: Wipe docker-compose.yml containers if: always() - run: docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.yml down + run: docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.yml down diff --git a/.github/workflows/iroha2-dev.yml b/.github/workflows/iroha2-dev.yml index 2e82a54c70b..c4f982c78eb 100644 --- a/.github/workflows/iroha2-dev.yml +++ b/.github/workflows/iroha2-dev.yml @@ -6,38 +6,41 @@ on: env: CARGO_TERM_COLOR: always - DOCKER_COMPOSE_PATH: defaults + DEFAULTS_DIR: defaults + WASM_TARGET_DIR: wasm/target/prebuilt ARTIFACTS_DIR: tmp/artifacts BIN_PATH: /usr/local/bin jobs: - build_executor: + build_wasm_libs: runs-on: ubuntu-latest container: image: hyperledger/iroha2-ci:nightly-2024-09-09 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - name: Build iroha executor - run: mold --run cargo run --bin iroha_wasm_builder -- build ./wasm_samples/default_executor --optimize --out-file ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm - - name: Upload executor to reuse in other jobs + - name: Build wasm libs + run: ./scripts/build_wasm.sh libs + - name: Upload wasm libs to reuse in other jobs uses: actions/upload-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + name: wasm-libs + path: ${{ env.WASM_TARGET_DIR }}/libs registry_save_build_artifacts: runs-on: [self-hosted, Linux, iroha2] container: image: hyperledger/iroha2-ci:nightly-2024-09-09 - needs: build_executor + needs: build_wasm_libs steps: - uses: actions/checkout@v4 - - name: Download executor.wasm file + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm-libs + path: ${{ env.DEFAULTS_DIR }}/libs + - name: Move the default executor + run: mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Set up Docker Buildx id: buildx if: always() diff --git a/.github/workflows/iroha2-release.yml b/.github/workflows/iroha2-release.yml index bb78307f6a8..9f6ac3658b1 100644 --- a/.github/workflows/iroha2-release.yml +++ b/.github/workflows/iroha2-release.yml @@ -7,35 +7,38 @@ on: env: CARGO_TERM_COLOR: always - DOCKER_COMPOSE_PATH: defaults + DEFAULTS_DIR: defaults + WASM_TARGET_DIR: wasm/target/prebuilt jobs: - build_executor: + build_wasm_libs: runs-on: ubuntu-latest container: image: hyperledger/iroha2-ci:nightly-2024-09-09 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - name: Build iroha executor - run: mold --run cargo run --bin iroha_wasm_builder -- build ./wasm_samples/default_executor --optimize --out-file ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm - - name: Upload executor to reuse in other jobs + - name: Build wasm libs + run: ./scripts/build_wasm.sh libs + - name: Upload wasm libs to reuse in other jobs uses: actions/upload-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }}/executor.wasm + name: wasm-libs + path: ${{ env.WASM_TARGET_DIR }}/libs retention-days: 1 registry: runs-on: ubuntu-latest - needs: build_executor + needs: build_wasm_libs steps: - uses: actions/checkout@v4 - - name: Download executor.wasm file + - name: Download wasm libs uses: actions/download-artifact@v4 with: - name: executor.wasm - path: ${{ env.DOCKER_COMPOSE_PATH }} + name: wasm-libs + path: ${{ env.DEFAULTS_DIR }}/libs + - name: Move the default executor + run: mv ${{ env.DEFAULTS_DIR }}/libs/default_executor.wasm ${{ env.DEFAULTS_DIR }}/executor.wasm - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 @@ -61,16 +64,16 @@ jobs: cache-to: type=gha,mode=max - name: Test docker-compose.single.yml before pushing run: | - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.single.yml up --wait || exit 1 - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.single.yml down + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.single.yml up --wait || exit 1 + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.single.yml down - name: Test docker-compose.local.yml before pushing run: | - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.local.yml up --wait || exit 1 - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.local.yml down + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.local.yml up --wait || exit 1 + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.local.yml down - name: Test docker-compose.yml before pushing run: | - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.yml up --wait || exit 1 - docker compose -f ${{ env.DOCKER_COMPOSE_PATH }}/docker-compose.yml down + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.yml up --wait || exit 1 + docker compose -f ${{ env.DEFAULTS_DIR }}/docker-compose.yml down - name: Login to DockerHub uses: docker/login-action@v3 with: diff --git a/Cargo.lock b/Cargo.lock index 850a3a1bb87..d975fb850fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2924,6 +2924,7 @@ dependencies = [ "iroha_executor_data_model", "iroha_genesis", "iroha_logger", + "iroha_multisig_data_model", "iroha_primitives", "iroha_telemetry", "iroha_test_network", @@ -2956,6 +2957,7 @@ dependencies = [ "erased-serde", "error-stack", "eyre", + "humantime", "iroha", "iroha_config_base", "iroha_primitives", @@ -3303,6 +3305,7 @@ dependencies = [ "eyre", "iroha_crypto", "iroha_data_model", + "iroha_executor_data_model", "iroha_schema", "iroha_test_samples", "parity-scale-codec", @@ -3368,6 +3371,17 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "iroha_multisig_data_model" +version = "2.0.0-rc.1.0" +dependencies = [ + "iroha_data_model", + "iroha_schema", + "parity-scale-codec", + "serde", + "serde_json", +] + [[package]] name = "iroha_numeric" version = "2.0.0-rc.1.0" @@ -3474,6 +3488,7 @@ dependencies = [ "iroha_data_model", "iroha_executor_data_model", "iroha_genesis", + "iroha_multisig_data_model", "iroha_primitives", "iroha_schema", ] diff --git a/Cargo.toml b/Cargo.toml index 6028d2e1b92..bd5bf0b5b3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ iroha_smart_contract_utils = { version = "=2.0.0-rc.1.0", path = "crates/iroha_s iroha_executor = { version = "=2.0.0-rc.1.0", path = "crates/iroha_executor" } iroha_executor_data_model = { version = "=2.0.0-rc.1.0", path = "crates/iroha_executor_data_model" } +iroha_multisig_data_model = { version = "=2.0.0-rc.1.0", path = "crates/iroha_multisig_data_model" } + iroha_test_network = { version = "=2.0.0-rc.1.0", path = "crates/iroha_test_network" } iroha_test_samples = { version = "=2.0.0-rc.1.0", path = "crates/iroha_test_samples" } @@ -86,6 +88,7 @@ owo-colors = "4.1.0" supports-color = "2.1.0" inquire = "0.6.2" spinoff = "0.8.0" +humantime = "2.1.0" criterion = "0.5.1" expect-test = "1.5.0" @@ -201,9 +204,6 @@ resolver = "2" members = [ "crates/*" ] -exclude = [ - "wasm_samples", -] [profile.deploy] inherits = "release" diff --git a/Dockerfile b/Dockerfile index 2c7f15773ec..9fb1819b8e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -82,6 +82,7 @@ COPY --from=builder $TARGET_DIR/iroha $BIN_PATH COPY --from=builder $TARGET_DIR/kagami $BIN_PATH COPY defaults/genesis.json $CONFIG_DIR COPY defaults/executor.wasm $CONFIG_DIR +COPY defaults/libs $CONFIG_DIR/ COPY defaults/client.toml $CONFIG_DIR USER $USER CMD ["irohad"] diff --git a/Dockerfile.glibc b/Dockerfile.glibc index 9349fb38096..41d865c2cd8 100644 --- a/Dockerfile.glibc +++ b/Dockerfile.glibc @@ -63,6 +63,7 @@ COPY --from=builder $TARGET_DIR/iroha $BIN_PATH COPY --from=builder $TARGET_DIR/kagami $BIN_PATH COPY defaults/genesis.json $CONFIG_DIR COPY defaults/executor.wasm $CONFIG_DIR +COPY defaults/libs $CONFIG_DIR/ COPY defaults/client.toml $CONFIG_DIR USER $USER CMD ["irohad"] diff --git a/README.md b/README.md index a2511eb2d3d..5e99fcb7099 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Iroha project mainly consists of the following crates: * [`iroha_logger`](crates/iroha_logger) uses `tracing` to provide logging facilities. * [`iroha_macro`](crates/iroha_macro) provides the convenience macros. * [`iroha_p2p`](crates/iroha_p2p) defines peer creation and handshake logic. -* [`iroha_default_executor`](wasm_samples/default_executor) defines runtime validation logic. +* [`iroha_default_executor`](wasm/libs/default_executor) defines runtime validation logic. * [`iroha_telemetry`](crates/iroha_telemetry) is used for monitoring and analysis of telemetry data. * [`iroha_version`](crates/iroha_version) provides message versioning for non-simultaneous system updates. diff --git a/crates/iroha/Cargo.toml b/crates/iroha/Cargo.toml index c729b7433dc..207c6ae6aa8 100644 --- a/crates/iroha/Cargo.toml +++ b/crates/iroha/Cargo.toml @@ -58,6 +58,7 @@ iroha_logger = { workspace = true } iroha_telemetry = { workspace = true } iroha_torii_const = { workspace = true } iroha_version = { workspace = true } +iroha_multisig_data_model = { workspace = true } attohttpc = { version = "0.28.0", default-features = false } eyre = { workspace = true } @@ -85,7 +86,7 @@ iroha_genesis = { workspace = true } iroha_test_samples = { workspace = true } iroha_test_network = { workspace = true } iroha_executor_data_model = { workspace = true } -executor_custom_data_model = { version = "=2.0.0-rc.1.0", path = "../../wasm_samples/executor_custom_data_model" } +executor_custom_data_model = { version = "=2.0.0-rc.1.0", path = "../../wasm/samples/executor_custom_data_model" } tokio = { workspace = true, features = ["rt-multi-thread"] } reqwest = { version = "0.12.7", features = ["json"] } diff --git a/crates/iroha/src/lib.rs b/crates/iroha/src/lib.rs index 31612c932f3..0daf1930e55 100644 --- a/crates/iroha/src/lib.rs +++ b/crates/iroha/src/lib.rs @@ -9,3 +9,4 @@ mod secrecy; pub use iroha_crypto as crypto; pub use iroha_data_model as data_model; +pub use iroha_multisig_data_model as multisig_data_model; diff --git a/crates/iroha/tests/multisig.rs b/crates/iroha/tests/multisig.rs index a4cc6f6f14b..84c6c453f9a 100644 --- a/crates/iroha/tests/multisig.rs +++ b/crates/iroha/tests/multisig.rs @@ -1,70 +1,213 @@ -use std::collections::BTreeMap; +use std::{ + collections::{BTreeMap, BTreeSet}, + time::Duration, +}; -use executor_custom_data_model::multisig::{MultisigArgs, MultisigRegisterArgs}; use eyre::Result; use iroha::{ + client::Client, crypto::KeyPair, - data_model::{ - asset::{AssetDefinition, AssetDefinitionId}, - parameter::SmartContractParameter, - prelude::*, - query::{builder::SingleQueryError, trigger::FindTriggers}, - transaction::TransactionBuilder, - }, + data_model::{prelude::*, query::trigger::FindTriggers, Level}, }; -use iroha_executor_data_model::permission::asset_definition::CanRegisterAssetDefinition; +use iroha_data_model::events::execute_trigger::ExecuteTriggerEventFilter; +use iroha_multisig_data_model::{MultisigAccountArgs, MultisigTransactionArgs}; use iroha_test_network::*; -use iroha_test_samples::{gen_account_in, load_sample_wasm, ALICE_ID}; -use nonzero_ext::nonzero; +use iroha_test_samples::{ + gen_account_in, ALICE_ID, BOB_ID, BOB_KEYPAIR, CARPENTER_ID, CARPENTER_KEYPAIR, +}; + +#[test] +fn multisig() -> Result<()> { + multisig_base(None) +} #[test] -fn mutlisig() -> Result<()> { - let (network, _rt) = NetworkBuilder::new() - .with_genesis_instruction(SetParameter::new(Parameter::SmartContract( - SmartContractParameter::Fuel(nonzero!(100_000_000_u64)), - ))) - .with_genesis_instruction(SetParameter::new(Parameter::Executor( - SmartContractParameter::Fuel(nonzero!(100_000_000_u64)), - ))) - .start_blocking()?; +fn multisig_expires() -> Result<()> { + multisig_base(Some(2)) +} + +/// # Scenario +/// +/// Proceeds from top left to bottom right. Starred operations are the responsibility of the user +/// +/// ``` +/// | world level | domain level | account level | transaction level | +/// |---------------------------|-----------------------------|---------------------------------|----------------------| +/// | given domains initializer | | | | +/// | | * creates domain | | | +/// | domains initializer | generates accounts registry | | | +/// | | | * creates signatories | | +/// | | * calls accounts registry | generates multisig account | | +/// | | accounts registry | generates transactions registry | | +/// | | | * calls transactions registry | proposes transaction | +/// | | | * calls transactions registry | approves transaction | +/// | | | transactions registry | executes transaction | +/// ``` +#[allow(clippy::cast_possible_truncation)] +fn multisig_base(transaction_ttl_ms: Option) -> Result<()> { + const N_SIGNATORIES: usize = 5; + + let (network, _rt) = NetworkBuilder::new().start_blocking()?; let test_client = network.client(); - let account_id = ALICE_ID.clone(); - let multisig_register_trigger_id = "multisig_register".parse::()?; - - let trigger = Trigger::new( - multisig_register_trigger_id.clone(), - Action::new( - load_sample_wasm("multisig_register"), - Repeats::Indefinitely, - account_id.clone(), - ExecuteTriggerEventFilter::new().for_trigger(multisig_register_trigger_id.clone()), - ), - ); - - // Register trigger which would allow multisig account creation in wonderland domain - // Access to call this trigger shouldn't be restricted - test_client.submit_blocking(Register::trigger(trigger))?; - - // Create multisig account id and destroy it's private key - let multisig_account_id = gen_account_in("wonderland").0; - - let multisig_trigger_id: TriggerId = format!( - "{}_{}_multisig_trigger", - multisig_account_id.signatory(), - multisig_account_id.domain() - ) - .parse()?; + let kingdom: DomainId = "kingdom".parse().unwrap(); + + // Assume some domain registered after genesis + let register_and_transfer_kingdom: [InstructionBox; 2] = [ + Register::domain(Domain::new(kingdom.clone())).into(), + Transfer::domain(ALICE_ID.clone(), kingdom.clone(), BOB_ID.clone()).into(), + ]; + test_client.submit_all_blocking(register_and_transfer_kingdom)?; - let signatories = core::iter::repeat_with(|| gen_account_in("wonderland")) - .take(5) + // One more block to generate a multisig accounts registry for the domain + test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; + + // Check that the multisig accounts registry has been generated + let multisig_accounts_registry_id = multisig_accounts_registry_of(&kingdom); + let _trigger = test_client + .query(FindTriggers::new()) + .filter_with(|trigger| trigger.id.eq(multisig_accounts_registry_id.clone())) + .execute_single() + .expect("multisig accounts registry should be generated after domain creation"); + + // Populate residents in the domain + let mut residents = core::iter::repeat_with(|| gen_account_in(&kingdom)) + .take(1 + N_SIGNATORIES) .collect::>(); + alt_client((BOB_ID.clone(), BOB_KEYPAIR.clone()), &test_client).submit_all_blocking( + residents + .keys() + .cloned() + .map(Account::new) + .map(Register::account), + )?; - let args = &MultisigRegisterArgs { - account: Account::new(multisig_account_id.clone()), - signatories: signatories.keys().cloned().collect(), + // Create a multisig account ID and discard the corresponding private key + let multisig_account_id = gen_account_in(&kingdom).0; + + let not_signatory = residents.pop_first().unwrap(); + let mut signatories = residents; + + let args = &MultisigAccountArgs { + account: multisig_account_id.signatory().clone(), + signatories: signatories + .keys() + .enumerate() + .map(|(weight, id)| (id.clone(), 1 + weight as u8)) + .collect(), + // Can be met without the first signatory + quorum: (1..=N_SIGNATORIES).skip(1).sum::() as u16, + transaction_ttl_ms: transaction_ttl_ms.unwrap_or(u64::MAX), }; + let register_multisig_account = + ExecuteTrigger::new(multisig_accounts_registry_id).with_args(args); + // Any account in another domain cannot register a multisig account without special permission + let _err = alt_client( + (CARPENTER_ID.clone(), CARPENTER_KEYPAIR.clone()), + &test_client, + ) + .submit_blocking(register_multisig_account.clone()) + .expect_err("multisig account should not be registered by account of another domain"); + + // Any account in the same domain can register a multisig account without special permission + alt_client(not_signatory, &test_client) + .submit_blocking(register_multisig_account) + .expect("multisig account should be registered by account of the same domain"); + + // Check that the multisig account has been registered + test_client + .query(FindAccounts::new()) + .filter_with(|account| account.id.eq(multisig_account_id.clone())) + .execute_single() + .expect("multisig account should be created by calling the multisig accounts registry"); + + // Check that the multisig transactions registry has been generated + let multisig_transactions_registry_id = multisig_transactions_registry_of(&multisig_account_id); + let _trigger = test_client + .query(FindTriggers::new()) + .filter_with(|trigger| trigger.id.eq(multisig_transactions_registry_id.clone())) + .execute_single() + .expect("multisig transactions registry should be generated along with the corresponding multisig account"); + + let key: Name = "key".parse().unwrap(); + let instructions = vec![SetKeyValue::account( + multisig_account_id.clone(), + key.clone(), + "value".parse::().unwrap(), + ) + .into()]; + let instructions_hash = HashOf::new(&instructions); + + let proposer = signatories.pop_last().unwrap(); + let approvers = signatories; + + let args = &MultisigTransactionArgs::Propose(instructions); + let propose = ExecuteTrigger::new(multisig_transactions_registry_id.clone()).with_args(args); + + alt_client(proposer, &test_client).submit_blocking(propose)?; + + // Check that the multisig transaction has not yet executed + let _err = test_client + .query_single(FindAccountMetadata::new( + multisig_account_id.clone(), + key.clone(), + )) + .expect_err("key-value shouldn't be set without enough approvals"); + + // Allow time to elapse to test the expiration + if let Some(ms) = transaction_ttl_ms { + std::thread::sleep(Duration::from_millis(ms)) + }; + test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; + + // All but the first signatory approve the multisig transaction + for approver in approvers.into_iter().skip(1) { + let args = &MultisigTransactionArgs::Approve(instructions_hash); + let approve = + ExecuteTrigger::new(multisig_transactions_registry_id.clone()).with_args(args); + + alt_client(approver, &test_client).submit_blocking(approve)?; + } + // Check that the multisig transaction has executed + let res = test_client.query_single(FindAccountMetadata::new( + multisig_account_id.clone(), + key.clone(), + )); + + if transaction_ttl_ms.is_some() { + let _err = res.expect_err("key-value shouldn't be set despite enough approvals"); + } else { + res.expect("key-value should be set with enough approvals"); + } + + Ok(()) +} + +/// # Scenario +/// +/// ``` +/// 012345 <--- root multisig account +/// / \ +/// / 12345 +/// / / \ +/// / 12 345 +/// / / \ / | \ +/// 0 1 2 3 4 5 <--- personal signatories +/// ``` +#[test] +#[allow(clippy::similar_names, clippy::too_many_lines)] +fn multisig_recursion() -> Result<()> { + let (network, _rt) = NetworkBuilder::new().start_blocking()?; + let test_client = network.client(); + + let wonderland = "wonderland"; + let ms_accounts_registry_id = multisig_accounts_registry_of(&wonderland.parse().unwrap()); + + // Populate signatories in the domain + let signatories = core::iter::repeat_with(|| gen_account_in(wonderland)) + .take(6) + .collect::>(); test_client.submit_all_blocking( signatories .keys() @@ -73,76 +216,275 @@ fn mutlisig() -> Result<()> { .map(Register::account), )?; - let call_trigger = ExecuteTrigger::new(multisig_register_trigger_id).with_args(args); - test_client.submit_blocking(call_trigger)?; + // Recursively register multisig accounts from personal signatories to the root one + let mut sigs = signatories.clone(); + let sigs_345 = sigs.split_off(signatories.keys().nth(3).unwrap()); + let sigs_12 = sigs.split_off(signatories.keys().nth(1).unwrap()); + let mut sigs_0 = sigs; - // Check that multisig account exist - test_client - .submit_blocking(Grant::account_permission( - CanRegisterAssetDefinition { - domain: "wonderland".parse().unwrap(), - }, - multisig_account_id.clone(), + let register_ms_accounts = |sigs_list: Vec>| { + sigs_list + .into_iter() + .map(|sigs| { + let ms_account_id = gen_account_in(wonderland).0; + let args = MultisigAccountArgs { + account: ms_account_id.signatory().clone(), + signatories: sigs.iter().copied().map(|id| (id.clone(), 1)).collect(), + quorum: sigs.len().try_into().unwrap(), + transaction_ttl_ms: u64::MAX, + }; + let register_ms_account = + ExecuteTrigger::new(ms_accounts_registry_id.clone()).with_args(&args); + + test_client + .submit_blocking(register_ms_account) + .expect("multisig account should be registered by account of the same domain"); + + ms_account_id + }) + .collect::>() + }; + + let sigs_list: Vec> = [&sigs_12, &sigs_345] + .into_iter() + .map(|sigs| sigs.keys().collect()) + .collect(); + let msas = register_ms_accounts(sigs_list); + let msa_12 = msas[0].clone(); + let msa_345 = msas[1].clone(); + + let sigs_list = vec![vec![&msa_12, &msa_345]]; + let msas = register_ms_accounts(sigs_list); + let msa_12345 = msas[0].clone(); + + let sig_0 = sigs_0.keys().next().unwrap().clone(); + let sigs_list = vec![vec![&sig_0, &msa_12345]]; + let msas = register_ms_accounts(sigs_list); + // The root multisig account with 6 personal signatories under its umbrella + let msa_012345 = msas[0].clone(); + + // One of personal signatories proposes a multisig transaction + let key: Name = "key".parse().unwrap(); + let instructions = vec![SetKeyValue::account( + msa_012345.clone(), + key.clone(), + "value".parse::().unwrap(), + ) + .into()]; + let instructions_hash = HashOf::new(&instructions); + + let proposer = sigs_0.pop_last().unwrap(); + let ms_transactions_registry_id = multisig_transactions_registry_of(&msa_012345); + let args = MultisigTransactionArgs::Propose(instructions); + let propose = ExecuteTrigger::new(ms_transactions_registry_id.clone()).with_args(&args); + + alt_client(proposer, &test_client).submit_blocking(propose)?; + + // Ticks as many times as the multisig recursion + (0..2).for_each(|_| { + test_client + .submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string())) + .unwrap(); + }); + + // Check that the entire authentication policy has been deployed down to one of the leaf registries + let approval_hash_to_12345 = { + let approval_hash_to_012345 = { + let registry_id = multisig_transactions_registry_of(&msa_012345); + let args = MultisigTransactionArgs::Approve(instructions_hash); + let approve: InstructionBox = ExecuteTrigger::new(registry_id.clone()) + .with_args(&args) + .into(); + + HashOf::new(&vec![approve]) + }; + let registry_id = multisig_transactions_registry_of(&msa_12345); + let args = MultisigTransactionArgs::Approve(approval_hash_to_012345); + let approve: InstructionBox = ExecuteTrigger::new(registry_id.clone()) + .with_args(&args) + .into(); + + HashOf::new(&vec![approve]) + }; + + let approvals_at_12: BTreeSet = test_client + .query_single(FindTriggerMetadata::new( + multisig_transactions_registry_of(&msa_12), + format!("proposals/{approval_hash_to_12345}/approvals") + .parse() + .unwrap(), )) - .expect("multisig account should be created after the call to register multisig trigger"); + .expect("leaf approvals should be initialized by the root proposal") + .try_into_any() + .unwrap(); + + assert!(1 == approvals_at_12.len() && approvals_at_12.contains(&msa_12345)); + + // Check that the multisig transaction has not yet executed + let _err = test_client + .query_single(FindAccountMetadata::new(msa_012345.clone(), key.clone())) + .expect_err("key-value shouldn't be set without enough approvals"); + + // All the rest signatories approve the multisig transaction + let approve_for_each = |approvers: BTreeMap, + instructions_hash: HashOf>, + ms_account: &AccountId| { + for approver in approvers { + let registry_id = multisig_transactions_registry_of(ms_account); + let args = MultisigTransactionArgs::Approve(instructions_hash); + let approve = ExecuteTrigger::new(registry_id.clone()).with_args(&args); + + alt_client(approver, &test_client) + .submit_blocking(approve) + .expect("should successfully approve the proposal"); + } + }; + + approve_for_each(sigs_12, approval_hash_to_12345, &msa_12); + approve_for_each(sigs_345, approval_hash_to_12345, &msa_345); + + // Let the intermediate registry (12345) collect approvals and approve the original proposal + test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; + + // Let the root registry (012345) collect approvals and execute the original proposal + test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; + + // Check that the multisig transaction has executed + test_client + .query_single(FindAccountMetadata::new(msa_012345.clone(), key.clone())) + .expect("key-value should be set with enough approvals"); + + Ok(()) +} + +#[test] +fn persistent_domain_level_authority() -> Result<()> { + let (network, _rt) = NetworkBuilder::new().start_blocking()?; + let test_client = network.client(); + + let wonderland: DomainId = "wonderland".parse().unwrap(); + + let ms_accounts_registry_id = multisig_accounts_registry_of(&wonderland); + + // Domain owner changes from Alice to Bob + test_client.submit_blocking(Transfer::domain( + ALICE_ID.clone(), + wonderland, + BOB_ID.clone(), + ))?; - // Check that multisig trigger exist - let trigger = test_client + // One block gap to follow the domain owner change + test_client.submit_blocking(Log::new(Level::DEBUG, "Just ticking time".to_string()))?; + + // Bob is the authority of the wonderland multisig accounts registry + let ms_accounts_registry = test_client .query(FindTriggers::new()) - .filter_with(|trigger| trigger.id.eq(multisig_trigger_id.clone())) + .filter_with(|trigger| trigger.id.eq(ms_accounts_registry_id.clone())) .execute_single() - .expect("multisig trigger should be created after the call to register multisig trigger"); + .expect("multisig accounts registry should survive before and after a domain owner change"); - assert_eq!(trigger.id(), &multisig_trigger_id); + assert!(*ms_accounts_registry.action().authority() == BOB_ID.clone()); - let asset_definition_id = "asset_definition_controlled_by_multisig#wonderland" - .parse::() - .unwrap(); - let isi = - vec![ - Register::asset_definition(AssetDefinition::numeric(asset_definition_id.clone())) - .into(), - ]; - let isi_hash = HashOf::new(&isi); - - let mut signatories_iter = signatories.into_iter(); - - if let Some((signatory, key_pair)) = signatories_iter.next() { - let args = &MultisigArgs::Instructions(isi); - let call_trigger = ExecuteTrigger::new(multisig_trigger_id.clone()).with_args(args); - test_client.submit_transaction_blocking( - &TransactionBuilder::new(test_client.chain.clone(), signatory) - .with_instructions([call_trigger]) - .sign(key_pair.private_key()), - )?; + Ok(()) +} + +#[test] +fn reserved_names() { + let (network, _rt) = NetworkBuilder::new().start_blocking().unwrap(); + let test_client = network.client(); + + let account_in_another_domain = gen_account_in("garden_of_live_flowers").0; + + { + let reserved_prefix = "multisig_accounts_"; + let register = { + let id: TriggerId = format!("{reserved_prefix}{}", account_in_another_domain.domain()) + .parse() + .unwrap(); + let action = Action::new( + Vec::::new(), + Repeats::Indefinitely, + ALICE_ID.clone(), + ExecuteTriggerEventFilter::new(), + ); + Register::trigger(Trigger::new(id, action)) + }; + let _err = test_client.submit_blocking(register).expect_err( + "trigger with this name shouldn't be registered by anyone other than multisig system", + ); } - // Check that asset definition isn't created yet - let err = test_client - .query(FindAssetsDefinitions::new()) - .filter_with(|asset_definition| asset_definition.id.eq(asset_definition_id.clone())) - .execute_single() - .expect_err("asset definition shouldn't be created before enough votes are collected"); - assert!(matches!(err, SingleQueryError::ExpectedOneGotNone)); - - for (signatory, key_pair) in signatories_iter { - let args = &MultisigArgs::Vote(isi_hash); - let call_trigger = ExecuteTrigger::new(multisig_trigger_id.clone()).with_args(args); - test_client.submit_transaction_blocking( - &TransactionBuilder::new(test_client.chain.clone(), signatory) - .with_instructions([call_trigger]) - .sign(key_pair.private_key()), - )?; + { + let reserved_prefix = "multisig_transactions_"; + let register = { + let id: TriggerId = format!( + "{reserved_prefix}{}_{}", + account_in_another_domain.signatory(), + account_in_another_domain.domain() + ) + .parse() + .unwrap(); + let action = Action::new( + Vec::::new(), + Repeats::Indefinitely, + ALICE_ID.clone(), + ExecuteTriggerEventFilter::new(), + ); + Register::trigger(Trigger::new(id, action)) + }; + let _err = test_client.submit_blocking(register).expect_err( + "trigger with this name shouldn't be registered by anyone other than domain owner", + ); } - // Check that new asset definition was created and multisig account is owner - let asset_definition = test_client - .query(FindAssetsDefinitions::new()) - .filter_with(|asset_definition| asset_definition.id.eq(asset_definition_id.clone())) - .execute_single() - .expect("asset definition should be created after enough votes are collected"); + { + let reserved_prefix = "multisig_signatory_"; + let register = { + let id: RoleId = format!( + "{reserved_prefix}{}_{}", + account_in_another_domain.signatory(), + account_in_another_domain.domain() + ) + .parse() + .unwrap(); + Register::role(Role::new(id, ALICE_ID.clone())) + }; + let _err = test_client.submit_blocking(register).expect_err( + "role with this name shouldn't be registered by anyone other than domain owner", + ); + } +} - assert_eq!(asset_definition.owned_by(), &multisig_account_id); +fn alt_client(signatory: (AccountId, KeyPair), base_client: &Client) -> Client { + Client { + account: signatory.0, + key_pair: signatory.1, + ..base_client.clone() + } +} - Ok(()) +fn multisig_accounts_registry_of(domain: &DomainId) -> TriggerId { + format!("multisig_accounts_{domain}",).parse().unwrap() +} + +fn multisig_transactions_registry_of(multisig_account: &AccountId) -> TriggerId { + format!( + "multisig_transactions_{}_{}", + multisig_account.signatory(), + multisig_account.domain() + ) + .parse() + .unwrap() +} + +#[allow(dead_code)] +fn debug_mst_registry(msa: &AccountId, client: &Client) { + let mst_registry = client + .query(FindTriggers::new()) + .filter_with(|trigger| trigger.id.eq(multisig_transactions_registry_of(msa))) + .execute_single() + .unwrap(); + let mst_metadata = mst_registry.action().metadata(); + + iroha_logger::error!(%msa, ?mst_metadata); } diff --git a/crates/iroha_cli/Cargo.toml b/crates/iroha_cli/Cargo.toml index 019db3bcd9b..8e253e4b697 100644 --- a/crates/iroha_cli/Cargo.toml +++ b/crates/iroha_cli/Cargo.toml @@ -35,6 +35,7 @@ thiserror = { workspace = true } error-stack = { workspace = true, features = ["eyre"] } eyre = { workspace = true } clap = { workspace = true, features = ["derive"] } +humantime = { workspace = true } json5 = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/iroha_cli/src/main.rs b/crates/iroha_cli/src/main.rs index 7da333bf27e..5aba6adc54d 100644 --- a/crates/iroha_cli/src/main.rs +++ b/crates/iroha_cli/src/main.rs @@ -110,6 +110,9 @@ enum Subcommand { Blocks(blocks::Args), /// The subcommand related to multi-instructions as Json or Json5 Json(json::Args), + /// The subcommand related to multisig accounts and transactions + #[clap(subcommand)] + Multisig(multisig::Args), } /// Context inside which command is executed @@ -165,7 +168,7 @@ macro_rules! match_all { impl RunArgs for Subcommand { fn run(self, context: &mut dyn RunContext) -> Result<()> { use Subcommand::*; - match_all!((self, context), { Domain, Account, Asset, Peer, Events, Wasm, Blocks, Json }) + match_all!((self, context), { Domain, Account, Asset, Peer, Events, Wasm, Blocks, Json, Multisig }) } } @@ -1190,6 +1193,238 @@ mod json { } } } + +mod multisig { + use std::io::{BufReader, Read as _}; + + use iroha::multisig_data_model::{ + MultisigAccountArgs, MultisigTransactionArgs, DEFAULT_MULTISIG_TTL_MS, + }; + + use super::*; + + /// Arguments for multisig subcommand + #[derive(Debug, clap::Subcommand)] + pub enum Args { + /// Register a multisig account + Register(Register), + /// Propose a multisig transaction + Propose(Propose), + /// Approve a multisig transaction + Approve(Approve), + /// List pending multisig transactions relevant to you + #[clap(subcommand)] + List(List), + } + + impl RunArgs for Args { + fn run(self, context: &mut dyn RunContext) -> Result<()> { + match_all!((self, context), { Args::Register, Args::Propose, Args::Approve, Args::List }) + } + } + /// Args to register a multisig account + #[derive(Debug, clap::Args)] + pub struct Register { + /// ID of the multisig account to be registered + #[arg(short, long)] + pub account: AccountId, + /// Signatories of the multisig account + #[arg(short, long, num_args(2..))] + pub signatories: Vec, + /// Relative weights of responsibility of respective signatories + #[arg(short, long, num_args(2..))] + pub weights: Vec, + /// Threshold of total weight at which the multisig is considered authenticated + #[arg(short, long)] + pub quorum: u16, + /// Time-to-live of multisig transactions made by the multisig account + #[arg(short, long, default_value_t = default_transaction_ttl())] + pub transaction_ttl: humantime::Duration, + } + + fn default_transaction_ttl() -> humantime::Duration { + std::time::Duration::from_millis(DEFAULT_MULTISIG_TTL_MS).into() + } + + impl RunArgs for Register { + fn run(self, context: &mut dyn RunContext) -> Result<()> { + let Self { + account, + signatories, + weights, + quorum, + transaction_ttl, + } = self; + if signatories.len() != weights.len() { + return Err(eyre!("signatories and weights must be equal in length")); + } + let registry_id: TriggerId = format!("multisig_accounts_{}", account.domain()) + .parse() + .unwrap(); + let args = MultisigAccountArgs { + account: account.signatory.clone(), + signatories: signatories.into_iter().zip(weights).collect(), + quorum, + transaction_ttl_ms: transaction_ttl + .as_millis() + .try_into() + .expect("ttl must be within 584942417 years"), + }; + let register_multisig_account = + iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args); + + submit([register_multisig_account], Metadata::default(), context) + .wrap_err("Failed to register multisig account") + } + } + + /// Args to propose a multisig transaction + #[derive(Debug, clap::Args)] + pub struct Propose { + /// Multisig authority of the multisig transaction + #[arg(short, long)] + pub account: AccountId, + } + + impl RunArgs for Propose { + fn run(self, context: &mut dyn RunContext) -> Result<()> { + let Self { account } = self; + let registry_id: TriggerId = format!( + "multisig_transactions_{}_{}", + account.signatory(), + account.domain() + ) + .parse() + .unwrap(); + let instructions: Vec = { + let mut reader = BufReader::new(stdin()); + let mut raw_content = Vec::new(); + reader.read_to_end(&mut raw_content)?; + let string_content = String::from_utf8(raw_content)?; + json5::from_str(&string_content)? + }; + let instructions_hash = HashOf::new(&instructions); + println!("{instructions_hash}"); + let args = MultisigTransactionArgs::Propose(instructions); + let propose_multisig_transaction = + iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args); + + submit([propose_multisig_transaction], Metadata::default(), context) + .wrap_err("Failed to propose transaction") + } + } + + /// Args to approve a multisig transaction + #[derive(Debug, clap::Args)] + pub struct Approve { + /// Multisig authority of the multisig transaction + #[arg(short, long)] + pub account: AccountId, + /// Instructions to approve + #[arg(short, long)] + pub instructions_hash: HashOf>, + } + + impl RunArgs for Approve { + fn run(self, context: &mut dyn RunContext) -> Result<()> { + let Self { + account, + instructions_hash, + } = self; + let registry_id: TriggerId = format!( + "multisig_transactions_{}_{}", + account.signatory(), + account.domain() + ) + .parse() + .unwrap(); + let args = MultisigTransactionArgs::Approve(instructions_hash); + let approve_multisig_transaction = + iroha::data_model::isi::ExecuteTrigger::new(registry_id).with_args(&args); + + submit([approve_multisig_transaction], Metadata::default(), context) + .wrap_err("Failed to approve transaction") + } + } + + /// List pending multisig transactions relevant to you + #[derive(clap::Subcommand, Debug, Clone)] + pub enum List { + /// All pending multisig transactions relevant to you + All, + } + + impl RunArgs for List { + fn run(self, context: &mut dyn RunContext) -> Result<()> { + let client = context.client_from_config(); + let me = client.account.clone(); + + trace_back_from(me, &client, context) + } + } + + /// Recursively trace back to the root multisig account + fn trace_back_from( + account: AccountId, + client: &Client, + context: &mut dyn RunContext, + ) -> Result<()> { + let Ok(multisig_roles) = client + .query(FindRolesByAccountId::new(account)) + .filter_with(|role_id| role_id.name.starts_with("multisig_signatory_")) + .execute_all() + else { + return Ok(()); + }; + + for role_id in multisig_roles { + let super_account: AccountId = role_id + .name + .as_ref() + .strip_prefix("multisig_signatory_") + .unwrap() + .replacen('_', "@", 1) + .parse() + .unwrap(); + + trace_back_from(super_account, client, context)?; + + let transactions_registry_id: TriggerId = role_id + .name + .as_ref() + .replace("signatory", "transactions") + .parse() + .unwrap(); + + context.print_data(&transactions_registry_id)?; + + let transactions_registry = client + .query(FindTriggers::new()) + .filter_with(|trigger| trigger.id.eq(transactions_registry_id)) + .execute_single()?; + let proposal_kvs = transactions_registry + .action() + .metadata() + .iter() + .filter(|kv| kv.0.as_ref().starts_with("proposals")); + + proposal_kvs.fold("", |acc, (k, v)| { + let mut path = k.as_ref().split('/'); + let hash = path.nth(1).unwrap(); + + if acc != hash { + context.print_data(&hash).unwrap(); + } + path.for_each(|seg| context.print_data(&seg).unwrap()); + context.print_data(&v).unwrap(); + + hash + }); + } + + Ok(()) + } +} #[cfg(test)] mod tests { use super::*; diff --git a/crates/iroha_core/src/kura.rs b/crates/iroha_core/src/kura.rs index 0c5f7fefe2d..e18725ddc37 100644 --- a/crates/iroha_core/src/kura.rs +++ b/crates/iroha_core/src/kura.rs @@ -1110,11 +1110,12 @@ mod tests { live_query_store, ); - let executor_path = PathBuf::from("../../defaults/executor.wasm").into(); - let genesis = GenesisBuilder::new(chain_id.clone(), executor_path) - .set_topology(topology.as_ref().to_owned()) - .build_and_sign(&genesis_key_pair) - .expect("genesis block should be built"); + let executor_path = "../../defaults/executor.wasm"; + let genesis = + GenesisBuilder::new(chain_id.clone(), executor_path, "wasm/libs/not/installed") + .set_topology(topology.as_ref().to_owned()) + .build_and_sign(&genesis_key_pair) + .expect("genesis block should be built"); { let mut state_block = state.block(genesis.0.header()); diff --git a/crates/iroha_crypto/src/hash.rs b/crates/iroha_crypto/src/hash.rs index a122ca05fd5..2c390b68e99 100644 --- a/crates/iroha_crypto/src/hash.rs +++ b/crates/iroha_crypto/src/hash.rs @@ -274,6 +274,14 @@ impl HashOf { } } +impl FromStr for HashOf { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + Ok(s.parse::().map(Self::from_untyped_unchecked)?) + } +} + impl IntoSchema for HashOf { fn type_name() -> String { format!("HashOf<{}>", T::type_name()) diff --git a/crates/iroha_data_model/src/block.rs b/crates/iroha_data_model/src/block.rs index 5eac540a25d..f76dc7aeba7 100644 --- a/crates/iroha_data_model/src/block.rs +++ b/crates/iroha_data_model/src/block.rs @@ -499,9 +499,9 @@ mod candidate { ); }; - if transactions.len() > 4 { + if transactions.len() > 5 { return Err( - "Genesis block must have 1 to 4 transactions (executor upgrade, initial topology, parameters, other isi)", + "Genesis block must have 1 to 5 transactions (executor upgrade, parameters, ordinary instructions, wasm trigger registrations, initial topology)", ); } diff --git a/crates/iroha_data_model/src/isi.rs b/crates/iroha_data_model/src/isi.rs index 4bf8ec295a3..45f5a3b318b 100644 --- a/crates/iroha_data_model/src/isi.rs +++ b/crates/iroha_data_model/src/isi.rs @@ -873,7 +873,7 @@ mod transparent { impl Revoke { /// Constructs a new [`Revoke`] for a [`Role`]. - pub fn role(role_id: RoleId, from: AccountId) -> Self { + pub fn account_role(role_id: RoleId, from: AccountId) -> Self { Self { object: role_id, destination: from, diff --git a/crates/iroha_executor/src/default.rs b/crates/iroha_executor/src/default.rs index 4155135e7a9..d3a3fa20029 100644 --- a/crates/iroha_executor/src/default.rs +++ b/crates/iroha_executor/src/default.rs @@ -368,7 +368,9 @@ pub mod domain { AnyPermission::CanRegisterTrigger(permission) => { permission.authority.domain() == domain_id } - AnyPermission::CanUnregisterTrigger(_) + AnyPermission::CanRegisterAnyTrigger(_) + | AnyPermission::CanUnregisterAnyTrigger(_) + | AnyPermission::CanUnregisterTrigger(_) | AnyPermission::CanExecuteTrigger(_) | AnyPermission::CanModifyTrigger(_) | AnyPermission::CanModifyTriggerMetadata(_) @@ -546,7 +548,9 @@ pub mod account { AnyPermission::CanBurnAsset(permission) => permission.asset.account() == account_id, AnyPermission::CanTransferAsset(permission) => permission.asset.account() == account_id, AnyPermission::CanRegisterTrigger(permission) => permission.authority == *account_id, - AnyPermission::CanUnregisterTrigger(_) + AnyPermission::CanRegisterAnyTrigger(_) + | AnyPermission::CanUnregisterAnyTrigger(_) + | AnyPermission::CanUnregisterTrigger(_) | AnyPermission::CanExecuteTrigger(_) | AnyPermission::CanModifyTrigger(_) | AnyPermission::CanModifyTriggerMetadata(_) @@ -812,6 +816,8 @@ pub mod asset_definition { AnyPermission::CanUnregisterAccount(_) | AnyPermission::CanRegisterAsset(_) | AnyPermission::CanModifyAccountMetadata(_) + | AnyPermission::CanRegisterAnyTrigger(_) + | AnyPermission::CanUnregisterAnyTrigger(_) | AnyPermission::CanRegisterTrigger(_) | AnyPermission::CanUnregisterTrigger(_) | AnyPermission::CanExecuteTrigger(_) @@ -1161,7 +1167,7 @@ pub mod parameter { } pub mod role { - use iroha_executor_data_model::permission::role::CanManageRoles; + use iroha_executor_data_model::permission::{role::CanManageRoles, trigger::CanExecuteTrigger}; use iroha_smart_contract::{data_model::role::Role, Iroha}; use super::*; @@ -1231,6 +1237,41 @@ pub mod role { let role = isi.object(); let mut new_role = Role::new(role.id().clone(), role.grant_to().clone()); + // Exception for multisig roles + let mut is_multisig_role = false; + if let Some(tail) = role + .id() + .name() + .as_ref() + .strip_prefix("multisig_signatory_") + { + let Ok(account_id) = tail.replacen('_', "@", 1).parse::() else { + deny!(executor, "Violates multisig role format") + }; + if crate::permission::domain::is_domain_owner( + account_id.domain(), + &executor.context().authority, + executor.host(), + ) + .unwrap_or_default() + { + // Bind this role to this permission here, regardless of the given contains + let permission = CanExecuteTrigger { + trigger: format!( + "multisig_transactions_{}_{}", + account_id.signatory(), + account_id.domain() + ) + .parse() + .unwrap(), + }; + new_role = new_role.add_permission(permission); + is_multisig_role = true; + } else { + deny!(executor, "Can't register multisig role") + } + } + for permission in role.inner().permissions() { iroha_smart_contract::debug!(&format!("Checking `{permission:?}`")); @@ -1257,9 +1298,9 @@ pub mod role { if executor.context().curr_block.is_genesis() || CanManageRoles.is_owned_by(&executor.context().authority, executor.host()) + || is_multisig_role { let grant_role = &Grant::account_role(role.id().clone(), role.grant_to().clone()); - let isi = &Register::role(new_role); if let Err(err) = executor.host().submit(isi) { executor.deny(err); @@ -1316,8 +1357,8 @@ pub mod role { pub mod trigger { use iroha_executor_data_model::permission::trigger::{ - CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterTrigger, - CanUnregisterTrigger, + CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterAnyTrigger, + CanRegisterTrigger, CanUnregisterAnyTrigger, CanUnregisterTrigger, }; use iroha_smart_contract::data_model::trigger::Trigger; @@ -1331,8 +1372,40 @@ pub mod trigger { isi: &Register, ) { let trigger = isi.object(); + let is_genesis = executor.context().curr_block.is_genesis(); + + let trigger_name = trigger.id().name().as_ref(); + + #[expect(clippy::option_if_let_else)] // clippy suggestion spoils readability + let naming_is_ok = if let Some(tail) = trigger_name.strip_prefix("multisig_accounts_") { + let system_account: AccountId = + // predefined in `GenesisBuilder::default` + "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system" + .parse() + .unwrap(); + tail.parse::().is_ok() + && (is_genesis || executor.context().authority == system_account) + } else if let Some(tail) = trigger_name.strip_prefix("multisig_transactions_") { + tail.replacen('_', "@", 1) + .parse::() + .ok() + .and_then(|account_id| { + is_domain_owner( + account_id.domain(), + &executor.context().authority, + executor.host(), + ) + .ok() + }) + .unwrap_or_default() + } else { + true + }; + if !naming_is_ok { + deny!(executor, "Violates trigger naming restrictions"); + } - if executor.context().curr_block.is_genesis() + if is_genesis || { match is_domain_owner( trigger.action().authority().domain(), @@ -1350,6 +1423,7 @@ pub mod trigger { can_register_user_trigger_token .is_owned_by(&executor.context().authority, executor.host()) } + || CanRegisterAnyTrigger.is_owned_by(&executor.context().authority, executor.host()) { execute!(executor, isi) } @@ -1374,6 +1448,7 @@ pub mod trigger { can_unregister_user_trigger_token .is_owned_by(&executor.context().authority, executor.host()) } + || CanUnregisterAnyTrigger.is_owned_by(&executor.context().authority, executor.host()) { let mut err = None; for (owner_id, permission) in accounts_permissions(executor.host()) { @@ -1470,7 +1545,8 @@ pub mod trigger { if executor.context().curr_block.is_genesis() { execute!(executor, isi); } - match is_trigger_owner(trigger_id, &executor.context().authority, executor.host()) { + let authority = &executor.context().authority; + match is_trigger_owner(trigger_id, authority, executor.host()) { Err(err) => deny!(executor, err), Ok(true) => execute!(executor, isi), Ok(false) => {} @@ -1478,7 +1554,21 @@ pub mod trigger { let can_execute_trigger_token = CanExecuteTrigger { trigger: trigger_id.clone(), }; - if can_execute_trigger_token.is_owned_by(&executor.context().authority, executor.host()) { + if can_execute_trigger_token.is_owned_by(authority, executor.host()) { + execute!(executor, isi); + } + // Any account in domain can call multisig accounts registry to register any multisig account in the domain + // TODO Restrict access to the multisig signatories? + // TODO Impose proposal and approval process? + if trigger_id + .name() + .as_ref() + .strip_prefix("multisig_accounts_") + .and_then(|s| s.parse::().ok()) + .map_or(false, |registry_domain| { + *authority.domain() == registry_domain + }) + { execute!(executor, isi); } @@ -1554,7 +1644,9 @@ pub mod trigger { AnyPermission::CanModifyTriggerMetadata(permission) => { &permission.trigger == trigger_id } - AnyPermission::CanRegisterTrigger(_) + AnyPermission::CanRegisterAnyTrigger(_) + | AnyPermission::CanUnregisterAnyTrigger(_) + | AnyPermission::CanRegisterTrigger(_) | AnyPermission::CanManagePeers(_) | AnyPermission::CanRegisterDomain(_) | AnyPermission::CanUnregisterDomain(_) diff --git a/crates/iroha_executor/src/permission.rs b/crates/iroha_executor/src/permission.rs index ebe7a873d0f..ed65e8185a0 100644 --- a/crates/iroha_executor/src/permission.rs +++ b/crates/iroha_executor/src/permission.rs @@ -117,6 +117,8 @@ declare_permissions! { iroha_executor_data_model::permission::parameter::{CanSetParameters}, iroha_executor_data_model::permission::role::{CanManageRoles}, + iroha_executor_data_model::permission::trigger::{CanRegisterAnyTrigger}, + iroha_executor_data_model::permission::trigger::{CanUnregisterAnyTrigger}, iroha_executor_data_model::permission::trigger::{CanRegisterTrigger}, iroha_executor_data_model::permission::trigger::{CanUnregisterTrigger}, iroha_executor_data_model::permission::trigger::{CanModifyTrigger}, @@ -754,8 +756,8 @@ pub mod account { pub mod trigger { //! Module with pass conditions for trigger related tokens use iroha_executor_data_model::permission::trigger::{ - CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterTrigger, - CanUnregisterTrigger, + CanExecuteTrigger, CanModifyTrigger, CanModifyTriggerMetadata, CanRegisterAnyTrigger, + CanRegisterTrigger, CanUnregisterAnyTrigger, CanUnregisterTrigger, }; use super::*; @@ -819,6 +821,34 @@ pub mod trigger { } } + impl ValidateGrantRevoke for CanRegisterAnyTrigger { + fn validate_grant(&self, authority: &AccountId, context: &Context, host: &Iroha) -> Result { + OnlyGenesis::from(self).validate(authority, host, context) + } + fn validate_revoke( + &self, + authority: &AccountId, + context: &Context, + host: &Iroha, + ) -> Result { + OnlyGenesis::from(self).validate(authority, host, context) + } + } + + impl ValidateGrantRevoke for CanUnregisterAnyTrigger { + fn validate_grant(&self, authority: &AccountId, context: &Context, host: &Iroha) -> Result { + OnlyGenesis::from(self).validate(authority, host, context) + } + fn validate_revoke( + &self, + authority: &AccountId, + context: &Context, + host: &Iroha, + ) -> Result { + OnlyGenesis::from(self).validate(authority, host, context) + } + } + impl ValidateGrantRevoke for CanRegisterTrigger { fn validate_grant(&self, authority: &AccountId, context: &Context, host: &Iroha) -> Result { super::account::Owner::from(self).validate(authority, host, context) diff --git a/crates/iroha_executor_data_model/src/permission.rs b/crates/iroha_executor_data_model/src/permission.rs index 27778496268..dc950a197bb 100644 --- a/crates/iroha_executor_data_model/src/permission.rs +++ b/crates/iroha_executor_data_model/src/permission.rs @@ -178,6 +178,16 @@ pub mod asset { pub mod trigger { use super::*; + permission! { + #[derive(Copy)] + pub struct CanRegisterAnyTrigger; + } + + permission! { + #[derive(Copy)] + pub struct CanUnregisterAnyTrigger; + } + permission! { pub struct CanRegisterTrigger { pub authority: AccountId, diff --git a/crates/iroha_genesis/Cargo.toml b/crates/iroha_genesis/Cargo.toml index 3167e2d7975..16a01ddbe2f 100644 --- a/crates/iroha_genesis/Cargo.toml +++ b/crates/iroha_genesis/Cargo.toml @@ -14,6 +14,7 @@ workspace = true iroha_crypto = { workspace = true } iroha_schema = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } +iroha_executor_data_model = { workspace = true } derive_more = { workspace = true, features = ["deref"] } serde = { workspace = true, features = ["derive"] } diff --git a/crates/iroha_genesis/src/lib.rs b/crates/iroha_genesis/src/lib.rs index ad9b899e76c..dce6936d78d 100644 --- a/crates/iroha_genesis/src/lib.rs +++ b/crates/iroha_genesis/src/lib.rs @@ -8,16 +8,35 @@ use std::{ sync::LazyLock, }; +use derive_more::Constructor; use eyre::{eyre, Result, WrapErr}; use iroha_crypto::{KeyPair, PublicKey}; use iroha_data_model::{block::SignedBlock, parameter::Parameter, peer::Peer, prelude::*}; +use iroha_executor_data_model::permission::trigger::{ + CanRegisterAnyTrigger, CanUnregisterAnyTrigger, +}; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; -/// [`DomainId`](iroha_data_model::domain::DomainId) of the genesis account. +/// Domain of the genesis account, technically required for the pre-genesis state pub static GENESIS_DOMAIN_ID: LazyLock = LazyLock::new(|| "genesis".parse().unwrap()); +/// Domain of the system account, implicitly registered in the genesis +pub static SYSTEM_DOMAIN_ID: LazyLock = LazyLock::new(|| "system".parse().unwrap()); + +/// The root authority for internal operations, implicitly registered in the genesis +// FIXME #5022 deny external access +// kagami crypto --seed "system" +pub static SYSTEM_ACCOUNT_ID: LazyLock = LazyLock::new(|| { + AccountId::new( + SYSTEM_DOMAIN_ID.clone(), + "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7" + .parse() + .unwrap(), + ) +}); + /// Genesis block. /// /// First transaction must contain single [`Upgrade`] instruction to set executor. @@ -35,19 +54,24 @@ pub struct RawGenesisTransaction { /// Unique id of blockchain chain: ChainId, /// Path to the [`Executor`] file - executor: ExecutorPath, + executor: WasmPath, /// Parameters #[serde(skip_serializing_if = "Option::is_none")] parameters: Option, + /// Instructions instructions: Vec, + /// Path to the directory that contains *.wasm libraries + wasm_dir: WasmPath, + /// Triggers whose executable is wasm, not instructions + wasm_triggers: Vec, /// Initial topology topology: Vec, } -/// Path to [`Executor`] file +/// Path to `*.wasm` file or their directory #[derive(Debug, Clone, Deserialize, Serialize, IntoSchema)] #[schema(transparent = "String")] -pub struct ExecutorPath(PathBuf); +pub struct WasmPath(PathBuf); impl RawGenesisTransaction { const WARN_ON_GENESIS_GTE: u64 = 1024 * 1024 * 1024; // 1Gb @@ -79,12 +103,17 @@ impl RawGenesisTransaction { let mut value: Self = serde_json::from_reader(reader).wrap_err_with(|| { eyre!( - "failed to deserialize raw genesis block from {}", + "failed to deserialize raw genesis transaction from {}", json_path.as_ref().display() ) })?; value.executor.resolve(here); + value.wasm_dir.resolve(here); + value + .wasm_triggers + .iter_mut() + .for_each(|trigger| trigger.action.executable.resolve(&value.wasm_dir.0)); Ok(value) } @@ -98,8 +127,10 @@ impl RawGenesisTransaction { GenesisBuilder { chain: self.chain, executor: self.executor, - instructions: self.instructions, parameters, + instructions: self.instructions, + wasm_dir: self.wasm_dir.0, + wasm_triggers: self.wasm_triggers, topology: self.topology, } } @@ -111,9 +142,15 @@ impl RawGenesisTransaction { /// Fails if [`RawGenesisTransaction::parse`] fails. pub fn build_and_sign(self, genesis_key_pair: &KeyPair) -> Result { let chain = self.chain.clone(); + let genesis_account = AccountId::new( + GENESIS_DOMAIN_ID.clone(), + genesis_key_pair.public_key().clone(), + ); let mut transactions = vec![]; for instructions in self.parse()? { - let transaction = build_transaction(instructions, chain.clone(), genesis_key_pair); + let transaction = TransactionBuilder::new(chain.clone(), genesis_account.clone()) + .with_instructions(instructions) + .sign(genesis_key_pair.private_key()); transactions.push(transaction); } let block = SignedBlock::genesis(transactions, genesis_key_pair.private_key()); @@ -129,7 +166,7 @@ impl RawGenesisTransaction { fn parse(self) -> Result>> { let mut instructions_list = vec![]; - let upgrade_executor = Upgrade::new(self.executor.try_into()?).into(); + let upgrade_executor = Upgrade::new(Executor::new(self.executor.try_into()?)).into(); instructions_list.push(vec![upgrade_executor]); if let Some(parameters) = self.parameters { @@ -145,6 +182,19 @@ impl RawGenesisTransaction { instructions_list.push(self.instructions); } + if !self.wasm_triggers.is_empty() { + let instructions = self + .wasm_triggers + .into_iter() + .map(Trigger::try_from) + .collect::>>()? + .into_iter() + .map(Register::trigger) + .map(InstructionBox::from) + .collect(); + instructions_list.push(instructions); + } + if !self.topology.is_empty() { let instructions = self .topology @@ -160,30 +210,16 @@ impl RawGenesisTransaction { } } -/// Build a transaction and sign as the genesis account. -fn build_transaction( - instructions: Vec, - chain: ChainId, - genesis_key_pair: &KeyPair, -) -> SignedTransaction { - let genesis_account = AccountId::new( - GENESIS_DOMAIN_ID.clone(), - genesis_key_pair.public_key().clone(), - ); - - TransactionBuilder::new(chain, genesis_account) - .with_instructions(instructions) - .sign(genesis_key_pair.private_key()) -} - /// Builder to build [`RawGenesisTransaction`] and [`GenesisBlock`]. /// No guarantee of validity of the built genesis transactions and block. #[must_use] pub struct GenesisBuilder { chain: ChainId, - executor: ExecutorPath, - instructions: Vec, + executor: WasmPath, parameters: Vec, + instructions: Vec, + wasm_dir: PathBuf, + wasm_triggers: Vec, topology: Vec, } @@ -191,25 +227,60 @@ pub struct GenesisBuilder { #[must_use] pub struct GenesisDomainBuilder { chain: ChainId, - executor: ExecutorPath, + executor: WasmPath, parameters: Vec, instructions: Vec, + wasm_dir: PathBuf, + wasm_triggers: Vec, topology: Vec, domain_id: DomainId, } impl GenesisBuilder { /// Construct [`GenesisBuilder`]. - pub fn new(chain: ChainId, executor: ExecutorPath) -> Self { + pub fn new(chain: ChainId, executor: impl Into, wasm_dir: impl Into) -> Self { Self { chain, - executor, + executor: executor.into().into(), parameters: Vec::new(), instructions: Vec::new(), + wasm_dir: wasm_dir.into(), + wasm_triggers: Vec::new(), topology: Vec::new(), } } + /// Entry system entities to serve standard functionality. + pub fn install_libs(self) -> Self { + // Register a trigger that reacts to domain creation (or owner changes) and registers (or replaces) a multisig accounts registry for the domain + let multisig_domains_initializer = GenesisWasmTrigger::new( + "multisig_domains".parse().unwrap(), + GenesisWasmAction::new( + "multisig_domains.wasm", + Repeats::Indefinitely, + SYSTEM_ACCOUNT_ID.clone(), + DomainEventFilter::new() + .for_events(DomainEventSet::Created | DomainEventSet::OwnerChanged), + ), + ); + let instructions = vec![ + Register::domain(Domain::new(SYSTEM_DOMAIN_ID.clone())).into(), + Register::account(Account::new(SYSTEM_ACCOUNT_ID.clone())).into(), + Grant::account_permission(CanRegisterAnyTrigger, SYSTEM_ACCOUNT_ID.clone()).into(), + Grant::account_permission(CanUnregisterAnyTrigger, SYSTEM_ACCOUNT_ID.clone()).into(), + ]; + + Self { + chain: self.chain, + executor: self.executor, + parameters: self.parameters, + instructions, + wasm_dir: self.wasm_dir, + wasm_triggers: vec![multisig_domains_initializer], + topology: self.topology, + } + } + /// Entry a domain registration and transition to [`GenesisDomainBuilder`]. pub fn domain(self, domain_name: Name) -> GenesisDomainBuilder { self.domain_with_metadata(domain_name, Metadata::default()) @@ -231,6 +302,8 @@ impl GenesisBuilder { executor: self.executor, parameters: self.parameters, instructions: self.instructions, + wasm_dir: self.wasm_dir, + wasm_triggers: self.wasm_triggers, topology: self.topology, domain_id, } @@ -248,6 +321,12 @@ impl GenesisBuilder { self } + /// Entry a wasm trigger to the end of entries. + pub fn append_wasm_trigger(mut self, wasm_trigger: GenesisWasmTrigger) -> Self { + self.wasm_triggers.push(wasm_trigger); + self + } + /// Overwrite the initial topology. pub fn set_topology(mut self, topology: Vec) -> Self { self.topology = topology; @@ -273,6 +352,8 @@ impl GenesisBuilder { executor: self.executor, parameters, instructions: self.instructions, + wasm_dir: self.wasm_dir.into(), + wasm_triggers: self.wasm_triggers, topology: self.topology, } } @@ -286,6 +367,8 @@ impl GenesisDomainBuilder { executor: self.executor, parameters: self.parameters, instructions: self.instructions, + wasm_dir: self.wasm_dir, + wasm_triggers: self.wasm_triggers, topology: self.topology, } } @@ -313,7 +396,7 @@ impl GenesisDomainBuilder { } } -impl Encode for ExecutorPath { +impl Encode for WasmPath { fn encode(&self) -> Vec { self.0 .to_str() @@ -322,32 +405,32 @@ impl Encode for ExecutorPath { } } -impl Decode for ExecutorPath { +impl Decode for WasmPath { fn decode( input: &mut I, ) -> std::result::Result { - String::decode(input).map(PathBuf::from).map(ExecutorPath) + String::decode(input).map(PathBuf::from).map(WasmPath) } } -impl From for ExecutorPath { +impl From for WasmPath { fn from(value: PathBuf) -> Self { Self(value) } } -impl TryFrom for Executor { +impl TryFrom for WasmSmartContract { type Error = eyre::Report; - fn try_from(value: ExecutorPath) -> Result { - let wasm = fs::read(&value.0) - .wrap_err_with(|| eyre!("failed to read the executor from {}", value.0.display()))?; + fn try_from(value: WasmPath) -> Result { + let blob = fs::read(&value.0) + .wrap_err_with(|| eyre!("failed to read wasm blob from {}", value.0.display()))?; - Ok(Executor::new(WasmSmartContract::from_compiled(wasm))) + Ok(WasmSmartContract::from_compiled(blob)) } } -impl ExecutorPath { +impl WasmPath { /// Resolve `self` to `here/self`, /// assuming `self` is an unresolved relative path to `here`. /// Must be applied once. @@ -356,6 +439,60 @@ impl ExecutorPath { } } +/// Human-readable alternative to [`Trigger`] whose action has wasm executable +#[derive(Debug, Clone, Serialize, Deserialize, IntoSchema, Encode, Decode, Constructor)] +pub struct GenesisWasmTrigger { + id: TriggerId, + action: GenesisWasmAction, +} + +/// Human-readable alternative to [`Action`] which has wasm executable +#[derive(Debug, Clone, Serialize, Deserialize, IntoSchema, Encode, Decode)] +pub struct GenesisWasmAction { + executable: WasmPath, + repeats: Repeats, + authority: AccountId, + filter: EventFilterBox, +} + +impl GenesisWasmAction { + /// Construct [`GenesisWasmAction`] + pub fn new( + executable: impl Into, + repeats: impl Into, + authority: AccountId, + filter: impl Into, + ) -> Self { + Self { + executable: executable.into().into(), + repeats: repeats.into(), + authority, + filter: filter.into(), + } + } +} + +impl TryFrom for Trigger { + type Error = eyre::Report; + + fn try_from(value: GenesisWasmTrigger) -> Result { + Ok(Trigger::new(value.id, value.action.try_into()?)) + } +} + +impl TryFrom for Action { + type Error = eyre::Report; + + fn try_from(value: GenesisWasmAction) -> Result { + Ok(Action::new( + WasmSmartContract::try_from(value.executable)?, + value.repeats, + value.authority, + value.filter, + )) + } +} + #[cfg(test)] mod tests { use iroha_test_samples::{ALICE_KEYPAIR, BOB_KEYPAIR}; @@ -363,19 +500,14 @@ mod tests { use super::*; - fn dummy_executor() -> (TempDir, ExecutorPath) { + fn test_builder() -> (TempDir, GenesisBuilder) { let tmp_dir = TempDir::new().unwrap(); - let wasm = WasmSmartContract::from_compiled(vec![1, 2, 3]); + let dummy_wasm = WasmSmartContract::from_compiled(vec![1, 2, 3]); let executor_path = tmp_dir.path().join("executor.wasm"); - std::fs::write(&executor_path, wasm).unwrap(); - - (tmp_dir, executor_path.into()) - } - - fn test_builder() -> (TempDir, GenesisBuilder) { - let (tmp_dir, executor_path) = dummy_executor(); + std::fs::write(&executor_path, dummy_wasm).unwrap(); let chain = ChainId::from("00000000-0000-0000-0000-000000000000"); - let builder = GenesisBuilder::new(chain, executor_path); + let wasm_dir = tmp_dir.path().join("wasm/"); + let builder = GenesisBuilder::new(chain, executor_path, wasm_dir); (tmp_dir, builder) } @@ -440,7 +572,7 @@ mod tests { assert_eq!( instructions[0], - Upgrade::new(executor_path.try_into()?).into() + Upgrade::new(Executor::new(executor_path.try_into()?)).into() ); assert_eq!(instructions.len(), 1); } @@ -522,11 +654,13 @@ mod tests { let genesis_json = format!( r#"{{ "chain": "0", - "executor": "./executor.wasm", + "executor": "executor.wasm", "parameters": {parameters}, "instructions": [], + "wasm_dir": "libs/", + "wasm_triggers": [], "topology": [] - }}"# + }}"# ); let _genesis: RawGenesisTransaction = diff --git a/crates/iroha_kagami/src/genesis/generate.rs b/crates/iroha_kagami/src/genesis/generate.rs index 07f58ec3f52..8ba89cc3519 100644 --- a/crates/iroha_kagami/src/genesis/generate.rs +++ b/crates/iroha_kagami/src/genesis/generate.rs @@ -9,17 +9,22 @@ use iroha_data_model::{isi::InstructionBox, parameter::Parameters, prelude::*}; use iroha_executor_data_model::permission::{ domain::CanRegisterDomain, parameter::CanSetParameters, }; -use iroha_genesis::{GenesisBuilder, RawGenesisTransaction, GENESIS_DOMAIN_ID}; +use iroha_genesis::{ + GenesisBuilder, GenesisWasmAction, GenesisWasmTrigger, RawGenesisTransaction, GENESIS_DOMAIN_ID, +}; use iroha_test_samples::{gen_account_in, ALICE_ID, BOB_ID, CARPENTER_ID}; use crate::{Outcome, RunArgs}; -/// Generate the genesis block that is used in tests +/// Generate a genesis configuration and standard-output in JSON format #[derive(Parser, Debug, Clone)] pub struct Args { - /// Specifies the `executor_file` that will be inserted into the genesis JSON as-is. + /// Relative path from the directory of output file to the executor.wasm file + #[clap(long, value_name = "PATH")] + executor: PathBuf, + /// Relative path from the directory of output file to the directory that contains *.wasm libraries #[clap(long, value_name = "PATH")] - executor_path_in_genesis: PathBuf, + wasm_dir: PathBuf, #[clap(long, value_name = "MULTI_HASH")] genesis_public_key: PublicKey, #[clap(subcommand)] @@ -54,13 +59,14 @@ pub enum Mode { impl RunArgs for Args { fn run(self, writer: &mut BufWriter) -> Outcome { let Self { - executor_path_in_genesis, + executor, + wasm_dir, genesis_public_key, mode, } = self; let chain = ChainId::from("00000000-0000-0000-0000-000000000000"); - let builder = GenesisBuilder::new(chain, executor_path_in_genesis.into()); + let builder = GenesisBuilder::new(chain, executor, wasm_dir).install_libs(); let genesis = match mode.unwrap_or_default() { Mode::Default => generate_default(builder, genesis_public_key), Mode::Synthetic { @@ -127,9 +133,8 @@ pub fn generate_default( ); let parameters = Parameters::default(); - let parameters = parameters.parameters(); - for parameter in parameters { + for parameter in parameters.parameters() { builder = builder.append_parameter(parameter); } @@ -146,6 +151,24 @@ pub fn generate_default( builder = builder.append_instruction(isi); } + // Manually register a multisig accounts registry for wonderland whose creation in genesis does not trigger the initializer + let multisig_accounts_registry_for_wonderland = { + let domain_owner = ALICE_ID.clone(); + let registry_id = "multisig_accounts_wonderland".parse::().unwrap(); + + GenesisWasmTrigger::new( + registry_id.clone(), + GenesisWasmAction::new( + "multisig_accounts.wasm", + Repeats::Indefinitely, + domain_owner, + ExecuteTriggerEventFilter::new().for_trigger(registry_id), + ), + ) + }; + + builder = builder.append_wasm_trigger(multisig_accounts_registry_for_wonderland); + Ok(builder.build_raw()) } diff --git a/crates/iroha_multisig_data_model/Cargo.toml b/crates/iroha_multisig_data_model/Cargo.toml new file mode 100644 index 00000000000..a104d502956 --- /dev/null +++ b/crates/iroha_multisig_data_model/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "iroha_multisig_data_model" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[lints] +workspace = true + +[dependencies] +iroha_data_model.workspace = true +iroha_schema.workspace = true + +parity-scale-codec = { workspace = true, features = ["derive"] } +serde.workspace = true +serde_json.workspace = true diff --git a/crates/iroha_multisig_data_model/src/lib.rs b/crates/iroha_multisig_data_model/src/lib.rs new file mode 100644 index 00000000000..6e83490cd62 --- /dev/null +++ b/crates/iroha_multisig_data_model/src/lib.rs @@ -0,0 +1,74 @@ +//! Arguments attached on executing triggers for multisig accounts or transactions + +#![no_std] + +extern crate alloc; + +use alloc::{collections::btree_map::BTreeMap, format, string::String, vec::Vec}; + +use iroha_data_model::prelude::*; +use iroha_schema::IntoSchema; +use parity_scale_codec::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +/// Arguments to register multisig account +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize, IntoSchema)] +pub struct MultisigAccountArgs { + /// Multisig account to be registered + ///
+ /// + /// Any corresponding private key allows the owner to manipulate this account as a ordinary personal account + /// + ///
+ // FIXME #5022 prevent multisig monopoly + // FIXME #5022 stop accepting user input: otherwise, after #4426 pre-registration account will be hijacked as a multisig account + pub account: PublicKey, + /// List of accounts and their relative weights of responsibility for the multisig + pub signatories: BTreeMap, + /// Threshold of total weight at which the multisig is considered authenticated + pub quorum: u16, + /// Multisig transaction time-to-live in milliseconds based on block timestamps. Defaults to [`DEFAULT_MULTISIG_TTL_MS`] + pub transaction_ttl_ms: u64, +} + +type Weight = u8; + +/// Default multisig transaction time-to-live in milliseconds based on block timestamps +pub const DEFAULT_MULTISIG_TTL_MS: u64 = 60 * 60 * 1_000; // 1 hour + +/// Arguments to propose or approve multisig transaction +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize, IntoSchema)] +pub enum MultisigTransactionArgs { + /// Propose instructions and initialize approvals with the proposer's one + Propose(Vec), + /// Approve certain instructions + Approve(HashOf>), +} + +impl From for Json { + fn from(details: MultisigAccountArgs) -> Self { + Json::new(details) + } +} + +impl TryFrom<&Json> for MultisigAccountArgs { + type Error = serde_json::Error; + + fn try_from(payload: &Json) -> serde_json::Result { + serde_json::from_str::(payload.as_ref()) + } +} + +impl From for Json { + fn from(details: MultisigTransactionArgs) -> Self { + Json::new(details) + } +} + +impl TryFrom<&Json> for MultisigTransactionArgs { + type Error = serde_json::Error; + + fn try_from(payload: &Json) -> serde_json::Result { + serde_json::from_str::(payload.as_ref()) + } +} diff --git a/crates/iroha_schema_gen/Cargo.toml b/crates/iroha_schema_gen/Cargo.toml index e1c1a416169..0c8a7511dc6 100644 --- a/crates/iroha_schema_gen/Cargo.toml +++ b/crates/iroha_schema_gen/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] iroha_data_model = { workspace = true, features = ["http", "transparent_api"] } iroha_executor_data_model = { workspace = true } +iroha_multisig_data_model = { workspace = true } iroha_primitives = { workspace = true } iroha_genesis = { workspace = true } diff --git a/crates/iroha_schema_gen/src/lib.rs b/crates/iroha_schema_gen/src/lib.rs index 1ac635ede44..8dbcc92fd01 100644 --- a/crates/iroha_schema_gen/src/lib.rs +++ b/crates/iroha_schema_gen/src/lib.rs @@ -35,6 +35,7 @@ macro_rules! types { pub fn build_schemas() -> MetaMap { use iroha_data_model::prelude::*; use iroha_executor_data_model::permission; + use iroha_multisig_data_model as multisig; macro_rules! schemas { ($($t:ty),* $(,)?) => {{ @@ -84,6 +85,8 @@ pub fn build_schemas() -> MetaMap { permission::asset::CanModifyAssetMetadata, permission::parameter::CanSetParameters, permission::role::CanManageRoles, + permission::trigger::CanRegisterAnyTrigger, + permission::trigger::CanUnregisterAnyTrigger, permission::trigger::CanRegisterTrigger, permission::trigger::CanExecuteTrigger, permission::trigger::CanUnregisterTrigger, @@ -91,6 +94,10 @@ pub fn build_schemas() -> MetaMap { permission::trigger::CanModifyTriggerMetadata, permission::executor::CanUpgradeExecutor, + // Arguments attached to multi-signature operations + multisig::MultisigAccountArgs, + multisig::MultisigTransactionArgs, + // Genesis file - used by SDKs to generate the genesis block // TODO: IMO it could/should be removed from the schema iroha_genesis::RawGenesisTransaction, @@ -157,6 +164,7 @@ types!( Box>, Box>, Box, + BTreeMap, BTreeMap, BTreeMap, BTreeSet, @@ -213,7 +221,7 @@ types!( ExecutorEvent, ExecutorEventFilter, ExecutorEventSet, - ExecutorPath, + WasmPath, ExecutorUpgrade, FetchSize, FindAccountMetadata, @@ -241,6 +249,8 @@ types!( FindRolesByAccountId, FindTriggerMetadata, ForwardCursor, + GenesisWasmAction, + GenesisWasmTrigger, GrantBox, Grant, Grant, @@ -249,6 +259,7 @@ types!( HashOf>, HashOf, HashOf, + HashOf>, IdBox, InstructionBox, InstructionEvaluationError, @@ -297,6 +308,8 @@ types!( Mint, Mint, Mismatch, + MultisigAccountArgs, + MultisigTransactionArgs, Name, NewAccount, NewAssetDefinition, @@ -475,6 +488,7 @@ types!( Vec>, Vec, Vec, + Vec, Vec, Vec, Vec, @@ -547,7 +561,8 @@ pub mod complete_data_model { }, Level, }; - pub use iroha_genesis::ExecutorPath; + pub use iroha_genesis::{GenesisWasmAction, GenesisWasmTrigger, WasmPath}; + pub use iroha_multisig_data_model::{MultisigAccountArgs, MultisigTransactionArgs}; pub use iroha_primitives::{ addr::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrHost, SocketAddrV4, SocketAddrV6}, const_vec::ConstVec, @@ -622,6 +637,12 @@ mod tests { insert_into_test_map!(iroha_executor_data_model::permission::asset::CanModifyAssetMetadata); insert_into_test_map!(iroha_executor_data_model::permission::parameter::CanSetParameters); insert_into_test_map!(iroha_executor_data_model::permission::role::CanManageRoles); + insert_into_test_map!( + iroha_executor_data_model::permission::trigger::CanRegisterAnyTrigger + ); + insert_into_test_map!( + iroha_executor_data_model::permission::trigger::CanUnregisterAnyTrigger + ); insert_into_test_map!(iroha_executor_data_model::permission::trigger::CanRegisterTrigger); insert_into_test_map!(iroha_executor_data_model::permission::trigger::CanExecuteTrigger); insert_into_test_map!(iroha_executor_data_model::permission::trigger::CanUnregisterTrigger); diff --git a/crates/iroha_swarm/src/lib.rs b/crates/iroha_swarm/src/lib.rs index 6074eb67e87..5584aa2948c 100644 --- a/crates/iroha_swarm/src/lib.rs +++ b/crates/iroha_swarm/src/lib.rs @@ -196,17 +196,14 @@ mod tests { - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -249,17 +246,14 @@ mod tests { - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -302,17 +296,14 @@ mod tests { - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -338,7 +329,6 @@ mod tests { - 1338:1338 - 8081:8081 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true irohad2: @@ -358,7 +348,6 @@ mod tests { - 1339:1339 - 8082:8082 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true irohad3: @@ -378,7 +367,6 @@ mod tests { - 1340:1340 - 8083:8083 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true "##]).assert_eq(&build_as_string( @@ -410,7 +398,6 @@ mod tests { - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -421,12 +408,10 @@ mod tests { start_period: 4s command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -466,7 +451,6 @@ mod tests { - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -477,12 +461,10 @@ mod tests { start_period: 4s command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -506,7 +488,6 @@ mod tests { - 1338:1338 - 8081:8081 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -530,7 +511,6 @@ mod tests { - 1339:1339 - 8082:8082 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -554,7 +534,6 @@ mod tests { - 1340:1340 - 8083:8083 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: diff --git a/crates/iroha_swarm/src/schema.rs b/crates/iroha_swarm/src/schema.rs index 787f5701b0d..3956a98bc1b 100644 --- a/crates/iroha_swarm/src/schema.rs +++ b/crates/iroha_swarm/src/schema.rs @@ -243,14 +243,12 @@ impl std::fmt::Display for ContainerFile<'_> { } } -const GENESIS_FILE: Filename = Filename("genesis.json"); const CONFIG_FILE: Filename = Filename("client.toml"); const GENESIS_SIGNED_SCALE: Filename = Filename("genesis.signed.scale"); const CONTAINER_CONFIG_DIR: ContainerPath = ContainerPath("/config/"); const CONTAINER_TMP_DIR: ContainerPath = ContainerPath("/tmp/"); -const CONTAINER_GENESIS_CONFIG: ContainerFile = ContainerFile(CONTAINER_CONFIG_DIR, GENESIS_FILE); const CONTAINER_CLIENT_CONFIG: ContainerFile = ContainerFile(CONTAINER_CONFIG_DIR, CONFIG_FILE); const CONTAINER_SIGNED_GENESIS: ContainerFile = ContainerFile(CONTAINER_TMP_DIR, GENESIS_SIGNED_SCALE); @@ -263,7 +261,7 @@ struct ReadOnly; struct PathMapping<'a>(HostFile<'a>, ContainerFile<'a>, ReadOnly); /// Mapping between host and container paths. -type Volumes<'a> = [PathMapping<'a>; 2]; +type Volumes<'a> = [PathMapping<'a>; 1]; /// Healthcheck parameters. #[derive(Debug)] @@ -328,12 +326,10 @@ where struct SignAndSubmitGenesis; const SIGN_AND_SUBMIT_GENESIS: &str = r#"/bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -532,18 +528,11 @@ impl<'a> DockerCompose<'a> { }: &'a PeerSettings, ) -> Self { let image = ImageId(name); - let volumes = [ - PathMapping( - HostFile(config_dir, GENESIS_FILE), - CONTAINER_GENESIS_CONFIG, - ReadOnly, - ), - PathMapping( - HostFile(config_dir, CONFIG_FILE), - CONTAINER_CLIENT_CONFIG, - ReadOnly, - ), - ]; + let volumes = [PathMapping( + HostFile(config_dir, CONFIG_FILE), + CONTAINER_CLIENT_CONFIG, + ReadOnly, + )]; Self { services: build_dir.as_ref().map_or_else( || { diff --git a/crates/iroha_test_network/src/config.rs b/crates/iroha_test_network/src/config.rs index 17a2eb69e18..eef0ca09c1e 100644 --- a/crates/iroha_test_network/src/config.rs +++ b/crates/iroha_test_network/src/config.rs @@ -41,19 +41,12 @@ pub fn genesis( topology: UniqueVec, ) -> GenesisBlock { // TODO: Fix this somehow. Probably we need to make `kagami` a library (#3253). - let genesis = match RawGenesisTransaction::from_path( - Path::new(env!("CARGO_MANIFEST_DIR")).join("../../defaults/genesis.json"), - ) { - Ok(x) => x, - Err(err) => { - eprintln!( - "ERROR: cannot load genesis from `defaults/genesis.json`\n \ - If `executor.wasm` is not found, make sure to run `scripts/build_wasm_samples.sh` first\n \ - Full error: {err}" - ); - panic!("cannot proceed without genesis, see the error above"); - } - }; + let json_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../defaults/genesis.json") + .canonicalize() + .unwrap(); + let genesis = RawGenesisTransaction::from_path(&json_path) + .unwrap_or_else(|err| panic!("failed to parse {}\n{err}", json_path.display())); let mut builder = genesis.into_builder(); let rose_definition_id = "rose#wonderland".parse::().unwrap(); @@ -92,5 +85,13 @@ pub fn genesis( builder .set_topology(topology.into()) .build_and_sign(&genesis_key_pair) - .expect("genesis should load fine") + .unwrap_or_else(|err| { + panic!( + "\ + failed to build a genesis block from {}\n\ + have you run `scripts/build_wasm.sh` to get wasm blobs?\n\ + {err}", + json_path.display() + ); + }) } diff --git a/crates/iroha_test_samples/src/lib.rs b/crates/iroha_test_samples/src/lib.rs index 3e2dd3ea1b4..ed1d29e45b2 100644 --- a/crates/iroha_test_samples/src/lib.rs +++ b/crates/iroha_test_samples/src/lib.rs @@ -95,12 +95,12 @@ fn read_file(path: impl AsRef) -> std::io::Result> { Ok(blob) } -const WASM_SAMPLES_PREBUILT_DIR: &str = "wasm_samples/target/prebuilt"; +const WASM_SAMPLES_PREBUILT_DIR: &str = "wasm/target/prebuilt/samples"; -/// Load WASM smart contract from `wasm_samples` by the name of smart contract, +/// Load WASM smart contract from `wasm/samples` by the name of smart contract, /// e.g. `default_executor`. /// -/// WASMs must be pre-built with the `build_wasm_samples.sh` script +/// WASMs must be pre-built with the `build_wasm.sh` script pub fn load_sample_wasm(name: impl AsRef) -> WasmSmartContract { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../") @@ -110,21 +110,19 @@ pub fn load_sample_wasm(name: impl AsRef) -> WasmSmartContract { .join(name.as_ref()) .with_extension("wasm"); - let blob = match read_file(&path) { + match read_file(&path) { Err(err) => { eprintln!( - "ERROR: Could not load sample WASM `{}` from `{}`: {err}\n \ - There are two possible reasons why:\n \ - 1. You haven't pre-built WASM samples before running tests. Make sure to run `build_wasm_samples.sh` first.\n \ - 2. `{}` is not a valid name. Check the `wasm_samples` directory and make sure you haven't made a mistake.", + "ERROR: Could not load sample WASM `{}` from `{}`: {err}\n\ + There are two possible reasons why:\n\ + 1. You haven't pre-built WASM samples before running tests. Make sure to run `build_wasm.sh` first.\n\ + 2. `{}` is not a valid name. Check the `wasm/samples` directory and make sure you haven't made a mistake.", name.as_ref(), path.display(), name.as_ref() ); panic!("could not build WASM, see the message above"); } - Ok(blob) => blob, - }; - - WasmSmartContract::from_compiled(blob) + Ok(blob) => WasmSmartContract::from_compiled(blob), + } } diff --git a/crates/irohad/src/main.rs b/crates/irohad/src/main.rs index 1f265caa4cc..f3c7235f723 100644 --- a/crates/irohad/src/main.rs +++ b/crates/irohad/src/main.rs @@ -710,7 +710,6 @@ mod tests { mod config_integration { use assertables::{assert_contains, assert_contains_as_result}; use iroha_crypto::{ExposedPrivateKey, KeyPair}; - use iroha_genesis::ExecutorPath; use iroha_primitives::addr::socket_addr; use iroha_version::Encode; use path_absolutize::Absolutize as _; @@ -732,19 +731,14 @@ mod tests { table } - fn dummy_executor() -> (TempDir, ExecutorPath) { + fn test_builder() -> (TempDir, GenesisBuilder) { let tmp_dir = TempDir::new().unwrap(); - let wasm = WasmSmartContract::from_compiled(vec![1, 2, 3]); + let dummy_wasm = WasmSmartContract::from_compiled(vec![1, 2, 3]); let executor_path = tmp_dir.path().join("executor.wasm"); - std::fs::write(&executor_path, wasm).unwrap(); - - (tmp_dir, executor_path.into()) - } - - fn test_builder() -> (TempDir, GenesisBuilder) { - let (tmp_dir, executor_path) = dummy_executor(); + std::fs::write(&executor_path, dummy_wasm).unwrap(); let chain = ChainId::from("00000000-0000-0000-0000-000000000000"); - let builder = GenesisBuilder::new(chain, executor_path); + let wasm_dir = tmp_dir.path().join("wasm/"); + let builder = GenesisBuilder::new(chain, executor_path, wasm_dir); (tmp_dir, builder) } diff --git a/defaults/docker-compose.local.yml b/defaults/docker-compose.local.yml index b75e03448c8..2402683161e 100644 --- a/defaults/docker-compose.local.yml +++ b/defaults/docker-compose.local.yml @@ -22,7 +22,6 @@ services: - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -33,12 +32,10 @@ services: start_period: 4s command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -64,7 +61,6 @@ services: - 1338:1338 - 8081:8081 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -90,7 +86,6 @@ services: - 1339:1339 - 8082:8082 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -116,7 +111,6 @@ services: - 1340:1340 - 8083:8083 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: diff --git a/defaults/docker-compose.single.yml b/defaults/docker-compose.single.yml index a38642f9f23..cf273212210 100644 --- a/defaults/docker-compose.single.yml +++ b/defaults/docker-compose.single.yml @@ -21,7 +21,6 @@ services: - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -32,12 +31,10 @@ services: start_period: 4s command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ diff --git a/defaults/docker-compose.yml b/defaults/docker-compose.yml index 960f34fb990..a7c33d916c9 100644 --- a/defaults/docker-compose.yml +++ b/defaults/docker-compose.yml @@ -20,7 +20,6 @@ services: - 1337:1337 - 8080:8080 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -31,12 +30,10 @@ services: start_period: 4s command: |- /bin/bash -c " - EXECUTOR_RELATIVE_PATH=$(jq -r '.executor' /config/genesis.json) && \\ - EXECUTOR_ABSOLUTE_PATH=$(realpath \"/config/$$EXECUTOR_RELATIVE_PATH\") && \\ + kagami genesis generate --executor ../config/executor.wasm --wasm-dir ../config/libs/ --genesis-public-key $$GENESIS_PUBLIC_KEY > /tmp/genesis.default.json jq \\ - --arg executor \"$$EXECUTOR_ABSOLUTE_PATH\" \\ --argjson topology \"$$TOPOLOGY\" \\ - '.executor = $$executor | .topology = $$topology' /config/genesis.json \\ + '.topology = $$topology' /tmp/genesis.default.json \\ >/tmp/genesis.json && \\ kagami genesis sign /tmp/genesis.json \\ --public-key $$GENESIS_PUBLIC_KEY \\ @@ -59,7 +56,6 @@ services: - 1338:1338 - 8081:8081 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -82,7 +78,6 @@ services: - 1339:1339 - 8082:8082 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: @@ -105,7 +100,6 @@ services: - 1340:1340 - 8083:8083 volumes: - - ./genesis.json:/config/genesis.json:ro - ./client.toml:/config/client.toml:ro init: true healthcheck: diff --git a/defaults/genesis.json b/defaults/genesis.json index ad3aff497fe..7d962c0e45d 100644 --- a/defaults/genesis.json +++ b/defaults/genesis.json @@ -1,6 +1,6 @@ { "chain": "00000000-0000-0000-0000-000000000000", - "executor": "./executor.wasm", + "executor": "executor.wasm", "parameters": { "sumeragi": { "block_time_ms": 2000, @@ -24,6 +24,45 @@ } }, "instructions": [ + { + "Register": { + "Domain": { + "id": "system", + "logo": null, + "metadata": {} + } + } + }, + { + "Register": { + "Account": { + "id": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system", + "metadata": {} + } + } + }, + { + "Grant": { + "Permission": { + "object": { + "name": "CanRegisterAnyTrigger", + "payload": null + }, + "destination": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system" + } + } + }, + { + "Grant": { + "Permission": { + "object": { + "name": "CanUnregisterAnyTrigger", + "payload": null + }, + "destination": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system" + } + } + }, { "Register": { "Domain": { @@ -151,5 +190,41 @@ } } ], + "wasm_dir": "libs/", + "wasm_triggers": [ + { + "id": "multisig_domains", + "action": { + "executable": "multisig_domains.wasm", + "repeats": "Indefinitely", + "authority": "ed0120D8B64D62FD8E09B9F29FE04D9C63E312EFB1CB29F1BF6AF00EBC263007AE75F7@system", + "filter": { + "Data": { + "Domain": { + "id_matcher": null, + "event_set": [ + "Created", + "OwnerChanged" + ] + } + } + } + } + }, + { + "id": "multisig_accounts_wonderland", + "action": { + "executable": "multisig_accounts.wasm", + "repeats": "Indefinitely", + "authority": "ed0120CE7FA46C9DCE7EA4B125E2E36BDB63EA33073E7590AC92816AE1E861B7048B03@wonderland", + "filter": { + "ExecuteTrigger": { + "trigger_id": "multisig_accounts_wonderland", + "authority": null + } + } + } + } + ], "topology": [] } diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index ecd4b45257b..7df957f5ba9 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -884,6 +884,7 @@ } ] }, + "CanRegisterAnyTrigger": null, "CanRegisterAsset": { "Struct": [ { @@ -941,6 +942,7 @@ } ] }, + "CanUnregisterAnyTrigger": null, "CanUnregisterAsset": { "Struct": [ { @@ -1998,6 +2000,38 @@ } ] }, + "GenesisWasmAction": { + "Struct": [ + { + "name": "executable", + "type": "String" + }, + { + "name": "repeats", + "type": "Repeats" + }, + { + "name": "authority", + "type": "AccountId" + }, + { + "name": "filter", + "type": "EventFilterBox" + } + ] + }, + "GenesisWasmTrigger": { + "Struct": [ + { + "name": "id", + "type": "TriggerId" + }, + { + "name": "action", + "type": "GenesisWasmAction" + } + ] + }, "Grant": { "Struct": [ { @@ -2057,6 +2091,7 @@ "HashOf": "Hash", "HashOf>": "Hash", "HashOf": "Hash", + "HashOf>": "Hash", "IdBox": { "Enum": [ { @@ -2571,6 +2606,40 @@ } ] }, + "MultisigAccountArgs": { + "Struct": [ + { + "name": "account", + "type": "PublicKey" + }, + { + "name": "signatories", + "type": "SortedMap" + }, + { + "name": "quorum", + "type": "u16" + }, + { + "name": "transaction_ttl_ms", + "type": "u64" + } + ] + }, + "MultisigTransactionArgs": { + "Enum": [ + { + "tag": "Propose", + "discriminant": 0, + "type": "Vec" + }, + { + "tag": "Approve", + "discriminant": 1, + "type": "HashOf>" + } + ] + }, "Name": "String", "NewAccount": { "Struct": [ @@ -3417,6 +3486,14 @@ "name": "instructions", "type": "Vec" }, + { + "name": "wasm_dir", + "type": "String" + }, + { + "name": "wasm_triggers", + "type": "Vec" + }, { "name": "topology", "type": "Vec" @@ -4184,6 +4261,12 @@ } ] }, + "SortedMap": { + "Map": { + "key": "AccountId", + "value": "u8" + } + }, "SortedMap": { "Map": { "key": "CustomParameterId", @@ -4954,6 +5037,9 @@ "Vec": { "Vec": "EventFilterBox" }, + "Vec": { + "Vec": "GenesisWasmTrigger" + }, "Vec": { "Vec": "InstructionBox" }, diff --git a/hooks/pre-commit.sample b/hooks/pre-commit.sample index 9735fc8994e..8e5fa0b9499 100755 --- a/hooks/pre-commit.sample +++ b/hooks/pre-commit.sample @@ -3,13 +3,13 @@ set -e # format checks cargo fmt --all -- --check -cargo fmt --manifest-path ./wasm_samples/Cargo.toml --all -- --check +cargo fmt --manifest-path ./wasm/Cargo.toml --all -- --check # lints cargo clippy --workspace --benches --tests --examples --all-features # TODO: fails, re-enable # cargo clippy --workspace --benches --tests --examples --no-default-features # update the default genesis, assuming the transaction authority is `iroha_test_samples::SAMPLE_GENESIS_ACCOUNT_ID` -cargo run --bin kagami -- genesis generate --executor-path-in-genesis ./executor.wasm --genesis-public-key ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 > ./defaults/genesis.json +cargo run --bin kagami -- genesis generate --executor executor.wasm --wasm-dir libs/ --genesis-public-key ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 > ./defaults/genesis.json # update schema cargo run --bin kagami -- schema > ./docs/source/references/schema.json # update docker compose files diff --git a/scripts/build_wasm.sh b/scripts/build_wasm.sh new file mode 100755 index 00000000000..ef95f6fbfce --- /dev/null +++ b/scripts/build_wasm.sh @@ -0,0 +1,63 @@ +#!/bin/sh +set -e; + +DEFAULTS_DIR="defaults" +CRATES_DIR="wasm" +TARGET_DIR="wasm/target/prebuilt" + +build() { + case $1 in + "libs") + NAMES=( + # order by dependency + "multisig_transactions" + "multisig_accounts" + "multisig_domains" + "default_executor" + ) + ;; + "samples") + NAMES=($( + cargo metadata --no-deps --manifest-path "$CRATES_DIR/Cargo.toml" --format-version=1 | + jq '.packages | map(select(.targets[].kind | contains(["cdylib"]))) | map(.manifest_path | split("/")) | map(select(.[-3] == "samples")) | map(.[-2]) | .[]' -r + )) + esac + + mkdir -p "$TARGET_DIR/$1" + for name in ${NAMES[@]}; do + out_file="$TARGET_DIR/$1/$name.wasm" + cargo run --bin iroha_wasm_builder -- build "$CRATES_DIR/$1/$name" --optimize --out-file "$out_file" + done + echo "info: WASM $1 build complete" + echo "artifacts written to $TARGET_DIR/$1/" +} + +command() { + case $1 in + "libs") + build $1 + cp -r "$TARGET_DIR/$1" "$DEFAULTS_DIR/$1" + mv "$DEFAULTS_DIR/$1/default_executor.wasm" "$DEFAULTS_DIR/executor.wasm" + echo "info: copied wasm $1 to $DEFAULTS_DIR/$1/" + echo "info: copied default executor to $DEFAULTS_DIR/executor.wasm" + ;; + "samples") + build $1 + esac +} + +case $1 in + "") + command "libs" + command "samples" + ;; + "libs") + command "libs" + ;; + "samples") + command "samples" + ;; + *) + echo "error: arg must be 'libs', 'samples', or empty to build both" + exit 1 +esac diff --git a/scripts/build_wasm_samples.sh b/scripts/build_wasm_samples.sh deleted file mode 100755 index 18d1d5117c8..00000000000 --- a/scripts/build_wasm_samples.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -set -e; -SAMPLES_DIR="wasm_samples" -TARGET_DIR="$SAMPLES_DIR/target/prebuilt" -mkdir -p "$TARGET_DIR" -for dir in $( - cargo metadata --no-deps --manifest-path ./wasm_samples/Cargo.toml --format-version=1 | - jq '.packages | map(select(.targets[].kind | contains(["cdylib"]))) | map(.manifest_path | split("/") | .[-2]) | .[]' -r -); do - out_file="$TARGET_DIR/$dir.wasm" - cargo run --bin iroha_wasm_builder -- build "$SAMPLES_DIR/$dir" --optimize --out-file "$out_file" -done -echo "info: WASM samples build complete" -cp "$TARGET_DIR/default_executor.wasm" ./defaults/executor.wasm -echo "info: copied default executor to ./defaults/executor.wasm" diff --git a/scripts/tests/consistency.sh b/scripts/tests/consistency.sh index 918ccef1a3a..ce8c9bca31b 100755 --- a/scripts/tests/consistency.sh +++ b/scripts/tests/consistency.sh @@ -3,8 +3,8 @@ set -e case $1 in "genesis") - cargo run --release --bin kagami -- genesis generate --executor-path-in-genesis ./executor.wasm --genesis-public-key ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 | diff - defaults/genesis.json || { - echo 'Please re-generate the default genesis with `cargo run --release --bin kagami -- genesis --executor-path-in-genesis ./executor.wasm --genesis-public-key ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 > ./defaults/genesis.json`' + cargo run --release --bin kagami -- genesis generate --executor executor.wasm --wasm-dir libs/ --genesis-public-key ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 | diff - defaults/genesis.json || { + echo 'Please re-generate the default genesis with `cargo run --release --bin kagami -- genesis --executor executor.wasm --wasm-dir libs/ --genesis-public-key ed01204164BF554923ECE1FD412D241036D863A6AE430476C898248B8237D77534CFC4 > ./defaults/genesis.json`' echo 'The assumption here is that the authority of the default genesis transaction is `iroha_test_samples::SAMPLE_GENESIS_ACCOUNT_ID`' exit 1 };; diff --git a/scripts/tests/instructions.json b/scripts/tests/instructions.json new file mode 100644 index 00000000000..5385f812f03 --- /dev/null +++ b/scripts/tests/instructions.json @@ -0,0 +1,11 @@ +[ + { + "SetKeyValue": { + "Account": { + "object": "ed01201F89368A4F322263C6F1AEF156759A83FB1AD7D93BAA66BFDFA973ACBADA462F@wonderland", + "key": "key", + "value": "congratulations" + } + } + } +] diff --git a/scripts/tests/multisig.recursion.sh b/scripts/tests/multisig.recursion.sh new file mode 100644 index 00000000000..fea4fc985cc --- /dev/null +++ b/scripts/tests/multisig.recursion.sh @@ -0,0 +1,95 @@ +#!/bin/sh +set -ex + +# This diagram describes the state when the root multisig account is successfully authenticated in this test: +# https://github.com/hyperledger/iroha/pull/5027#discussion_r1741722664 + +cargo build +scripts/test_env.py setup +cd test + +gen_key_pair() { + ./kagami crypto -cs $1 +} + +DOMAIN="wonderland" + +gen_account_id() { + public_key=$(gen_key_pair $1 | head -n 1) + echo "$public_key@$DOMAIN" +} + +gen_signatories() { + for n in $(seq 1 $1); do + i=$((n-1)) + key_pair=($(gen_key_pair $i)) + public_key=${key_pair[0]} + private_key=${key_pair[1]} + # yield an account ID + echo "$public_key@$DOMAIN" + # generate a config + cat client.toml | sed '/domain/d' | sed '/public_key/d' | sed '/private_key/d' > client.$i.toml + echo "domain = \"$DOMAIN\"" >> client.$i.toml + echo "public_key = \"$public_key\"" >> client.$i.toml + echo "private_key = \"$private_key\"" >> client.$i.toml + done +} + +# populate signatories +N_SIGNATORIES=6 +SIGNATORIES=($(gen_signatories $N_SIGNATORIES)) +for signatory in ${SIGNATORIES[@]}; do + ./iroha account register --id $signatory +done +WEIGHTS=($(yes 1 | head -n $N_SIGNATORIES)) + +# register a multisig account, namely msa12 +MSA_12=$(gen_account_id "msa12") +SIGS_12=(${SIGNATORIES[@]:1:2}) +./iroha multisig register --account $MSA_12 --signatories ${SIGS_12[*]} --weights 1 1 --quorum 2 + +# register a multisig account, namely msa345 +MSA_345=$(gen_account_id "msa345") +SIGS_345=(${SIGNATORIES[@]:3:3}) +./iroha multisig register --account $MSA_345 --signatories ${SIGS_345[*]} --weights 1 1 1 --quorum 1 + +# register a multisig account, namely msa12345 +MSA_12345=$(gen_account_id "msa12345") +SIGS_12345=($MSA_12 $MSA_345) +./iroha multisig register --account $MSA_12345 --signatories ${SIGS_12345[*]} --weights 1 1 --quorum 1 + +# register a multisig account, namely msa012345 +MSA_012345=$(gen_account_id "msa") +SIGS_012345=(${SIGNATORIES[0]} $MSA_12345) +./iroha multisig register --account $MSA_012345 --signatories ${SIGS_012345[*]} --weights 1 1 --quorum 2 + +# propose a multisig transaction +INSTRUCTIONS="../scripts/tests/instructions.json" +propose_stdout=($(cat $INSTRUCTIONS | ./iroha --config "client.0.toml" multisig propose --account $MSA_012345)) +INSTRUCTIONS_HASH=${propose_stdout[0]} + +# ticks as many times as the multisig recursion +TICK="../scripts/tests/tick.json" +for i in $(seq 0 1); do + cat $TICK | ./iroha json transaction +done + +# check that one of the leaf signatories is involved +LIST=$(./iroha --config "client.5.toml" multisig list all) +echo "$LIST" | grep $INSTRUCTIONS_HASH + +# approve the multisig transaction +HASH_TO_12345=$(echo "$LIST" | grep -A1 "multisig_transactions" | sed 's/_/@/g' | grep -A1 $MSA_345 | tail -n 1 | tr -d '"') +./iroha --config "client.5.toml" multisig approve --account $MSA_345 --instructions-hash $HASH_TO_12345 + +# ticks as many times as the multisig recursion +for i in $(seq 0 1); do + cat $TICK | ./iroha json transaction +done + +# check that the multisig transaction is executed +./iroha account list all | grep "congratulations" +! ./iroha --config "client.5.toml" multisig list all | grep $INSTRUCTIONS_HASH + +cd - +scripts/test_env.py cleanup diff --git a/scripts/tests/multisig.sh b/scripts/tests/multisig.sh new file mode 100644 index 00000000000..272e52b0cfb --- /dev/null +++ b/scripts/tests/multisig.sh @@ -0,0 +1,66 @@ +#!/bin/sh +set -ex + +cargo build +scripts/test_env.py setup +cd test + +gen_key_pair() { + ./kagami crypto -cs $1 +} + +DOMAIN="wonderland" + +gen_account_id() { + public_key=$(gen_key_pair $1 | head -n 1) + echo "$public_key@$DOMAIN" +} + +gen_signatories() { + for i in $(seq 1 $1); do + key_pair=($(gen_key_pair $i)) + public_key=${key_pair[0]} + private_key=${key_pair[1]} + # yield an account ID + echo "$public_key@$DOMAIN" + # generate a config + cat client.toml | sed '/domain/d' | sed '/public_key/d' | sed '/private_key/d' > client.$i.toml + echo "domain = \"$DOMAIN\"" >> client.$i.toml + echo "public_key = \"$public_key\"" >> client.$i.toml + echo "private_key = \"$private_key\"" >> client.$i.toml + done +} + +# populate signatories +N_SIGNATORIES=3 +SIGNATORIES=($(gen_signatories $N_SIGNATORIES)) +for signatory in ${SIGNATORIES[@]}; do + ./iroha account register --id $signatory +done + +# register a multisig account +MULTISIG_ACCOUNT=$(gen_account_id "msa") +WEIGHTS=($(yes 1 | head -n $N_SIGNATORIES)) # equal votes +QUORUM=$N_SIGNATORIES # unanimous +TRANSACTION_TTL="1y 6M 2w 3d 12h 30m 30s 500ms" +./iroha --config "client.1.toml" multisig register --account $MULTISIG_ACCOUNT --signatories ${SIGNATORIES[*]} --weights ${WEIGHTS[*]} --quorum $QUORUM --transaction-ttl "$TRANSACTION_TTL" + +# propose a multisig transaction +INSTRUCTIONS="../scripts/tests/instructions.json" +propose_stdout=($(cat $INSTRUCTIONS | ./iroha --config "client.1.toml" multisig propose --account $MULTISIG_ACCOUNT)) +INSTRUCTIONS_HASH=${propose_stdout[0]} + +# check that 2nd signatory is involved +./iroha --config "client.2.toml" multisig list all | grep $INSTRUCTIONS_HASH + +# approve the multisig transaction +for i in $(seq 2 $N_SIGNATORIES); do + ./iroha --config "client.$i.toml" multisig approve --account $MULTISIG_ACCOUNT --instructions-hash $INSTRUCTIONS_HASH +done + +# check that the multisig transaction is executed +./iroha account list all | grep "congratulations" +! ./iroha --config "client.2.toml" multisig list all | grep $INSTRUCTIONS_HASH + +cd - +scripts/test_env.py cleanup diff --git a/scripts/tests/tick.json b/scripts/tests/tick.json new file mode 100644 index 00000000000..2d8e1cfac86 --- /dev/null +++ b/scripts/tests/tick.json @@ -0,0 +1,8 @@ +[ + { + "Log": { + "DEBUG": null, + "msg": "Just ticking time" + } + } +] diff --git a/wasm_samples/.cargo/config.toml b/wasm/.cargo/config.toml similarity index 100% rename from wasm_samples/.cargo/config.toml rename to wasm/.cargo/config.toml diff --git a/wasm_samples/Cargo.toml b/wasm/Cargo.toml similarity index 74% rename from wasm_samples/Cargo.toml rename to wasm/Cargo.toml index 6ebcb8fb9e5..8a4be9978dc 100644 --- a/wasm_samples/Cargo.toml +++ b/wasm/Cargo.toml @@ -9,22 +9,8 @@ license = "Apache-2.0" [workspace] resolver = "2" members = [ - "default_executor", - "create_nft_for_every_user_trigger", - "mint_rose_trigger", - "mint_rose_trigger_args", - "executor_with_admin", - "executor_with_custom_permission", - "executor_with_custom_parameter", - "executor_remove_permission", - "executor_with_migration_fail", - "executor_custom_instructions_simple", - "executor_custom_instructions_complex", - "executor_custom_data_model", - "query_assets_and_save_cursor", - "smart_contract_can_filter_queries", - "multisig_register", - "multisig", + "libs/*", + "samples/*", ] [profile.dev] @@ -38,7 +24,7 @@ opt-level = "z" # Optimize for size vs speed with "s"/"z"(removes vectorizat codegen-units = 1 # Further reduces binary size but increases compilation time [workspace.dependencies] -executor_custom_data_model = { path = "executor_custom_data_model" } +executor_custom_data_model = { path = "samples/executor_custom_data_model" } iroha_smart_contract = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_smart_contract", features = ["debug"] } iroha_trigger = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_trigger", features = ["debug"] } @@ -46,6 +32,7 @@ iroha_executor = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_executor", iroha_schema = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_schema" } iroha_data_model = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_data_model", default-features = false } iroha_executor_data_model = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_executor_data_model" } +iroha_multisig_data_model = { version = "=2.0.0-rc.1.0", path = "../crates/iroha_multisig_data_model" } parity-scale-codec = { version = "3.2.1", default-features = false } anyhow = { version = "1.0.71", default-features = false } diff --git a/wasm_samples/LICENSE b/wasm/LICENSE similarity index 100% rename from wasm_samples/LICENSE rename to wasm/LICENSE diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 00000000000..d140fdb54e7 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,21 @@ +# How to build + +From the project root, run: + +## All WASM crates + +```bash +bash scripts/build_wasm.sh +``` + +## WASM libraries only + +```bash +bash scripts/build_wasm.sh libs +``` + +## WASM samples only + +```bash +bash scripts/build_wasm.sh samples +``` diff --git a/wasm_samples/default_executor/Cargo.toml b/wasm/libs/default_executor/Cargo.toml similarity index 100% rename from wasm_samples/default_executor/Cargo.toml rename to wasm/libs/default_executor/Cargo.toml diff --git a/wasm_samples/default_executor/README.md b/wasm/libs/default_executor/README.md similarity index 65% rename from wasm_samples/default_executor/README.md rename to wasm/libs/default_executor/README.md index f273a8dac61..c41a648adf1 100644 --- a/wasm_samples/default_executor/README.md +++ b/wasm/libs/default_executor/README.md @@ -4,5 +4,5 @@ Use the [Wasm Builder CLI](../../crates/iroha_wasm_builder) in order to build it ```bash cargo run --bin iroha_wasm_builder -- \ - build ./wasm_samples/default_executor --optimize --out-file ./defaults/executor.wasm + build ./wasm/libs/default_executor --optimize --out-file ./defaults/executor.wasm ``` diff --git a/wasm_samples/default_executor/src/lib.rs b/wasm/libs/default_executor/src/lib.rs similarity index 100% rename from wasm_samples/default_executor/src/lib.rs rename to wasm/libs/default_executor/src/lib.rs diff --git a/wasm_samples/multisig_register/Cargo.toml b/wasm/libs/multisig_accounts/Cargo.toml similarity index 69% rename from wasm_samples/multisig_register/Cargo.toml rename to wasm/libs/multisig_accounts/Cargo.toml index 042a19d6e53..476b201367d 100644 --- a/wasm_samples/multisig_register/Cargo.toml +++ b/wasm/libs/multisig_accounts/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "multisig_register" +name = "multisig_accounts" edition.workspace = true version.workspace = true @@ -13,7 +13,7 @@ crate-type = ['cdylib'] [dependencies] iroha_trigger.workspace = true iroha_executor_data_model.workspace = true -executor_custom_data_model.workspace = true +iroha_multisig_data_model.workspace = true panic-halt.workspace = true dlmalloc.workspace = true @@ -21,6 +21,3 @@ getrandom.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, default-features = false } - -[build-dependencies] -iroha_wasm_builder = { version = "=2.0.0-rc.1.0", path = "../../crates/iroha_wasm_builder" } diff --git a/wasm/libs/multisig_accounts/src/lib.rs b/wasm/libs/multisig_accounts/src/lib.rs new file mode 100644 index 00000000000..7ad18e3a2f7 --- /dev/null +++ b/wasm/libs/multisig_accounts/src/lib.rs @@ -0,0 +1,155 @@ +//! Trigger given per domain to control multi-signature accounts and corresponding triggers + +#![no_std] + +extern crate alloc; +#[cfg(not(test))] +extern crate panic_halt; + +use alloc::format; + +use dlmalloc::GlobalDlmalloc; +use iroha_executor_data_model::permission::trigger::CanExecuteTrigger; +use iroha_multisig_data_model::MultisigAccountArgs; +use iroha_trigger::{ + debug::{dbg_panic, DebugExpectExt as _}, + prelude::*, +}; + +#[global_allocator] +static ALLOC: GlobalDlmalloc = GlobalDlmalloc; + +getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); + +// Binary containing common logic to each multisig account for handling multisig transactions +const MULTISIG_TRANSACTIONS_WASM: &[u8] = core::include_bytes!(concat!( + core::env!("CARGO_MANIFEST_DIR"), + "/../../target/prebuilt/libs/multisig_transactions.wasm" +)); + +#[iroha_trigger::main] +fn main(host: Iroha, context: Context) { + let EventBox::ExecuteTrigger(event) = context.event else { + dbg_panic("trigger misused: must be triggered only by a call"); + }; + let args: MultisigAccountArgs = event + .args() + .try_into_any() + .dbg_expect("args should be for a multisig account"); + let domain_id = context + .id + .name() + .as_ref() + .strip_prefix("multisig_accounts_") + .and_then(|s| s.parse::().ok()) + .dbg_unwrap(); + let account_id = AccountId::new(domain_id, args.account); + + host.submit(&Register::account(Account::new(account_id.clone()))) + .dbg_expect("accounts registry should successfully register a multisig account"); + + let multisig_transactions_registry_id: TriggerId = format!( + "multisig_transactions_{}_{}", + account_id.signatory(), + account_id.domain() + ) + .parse() + .dbg_unwrap(); + + let multisig_transactions_registry = Trigger::new( + multisig_transactions_registry_id.clone(), + Action::new( + WasmSmartContract::from_compiled(MULTISIG_TRANSACTIONS_WASM.to_vec()), + Repeats::Indefinitely, + account_id.clone(), + ExecuteTriggerEventFilter::new().for_trigger(multisig_transactions_registry_id.clone()), + ), + ); + + host.submit(&Register::trigger(multisig_transactions_registry)) + .dbg_expect("accounts registry should successfully register a transactions registry"); + + host.submit(&SetKeyValue::trigger( + multisig_transactions_registry_id.clone(), + "signatories".parse().unwrap(), + Json::new(&args.signatories), + )) + .dbg_unwrap(); + + host.submit(&SetKeyValue::trigger( + multisig_transactions_registry_id.clone(), + "quorum".parse().unwrap(), + Json::new(&args.quorum), + )) + .dbg_unwrap(); + + host.submit(&SetKeyValue::trigger( + multisig_transactions_registry_id.clone(), + "transaction_ttl_ms".parse().unwrap(), + Json::new(&args.transaction_ttl_ms), + )) + .dbg_unwrap(); + + let role_id: RoleId = format!( + "multisig_signatory_{}_{}", + account_id.signatory(), + account_id.domain() + ) + .parse() + .dbg_unwrap(); + + host.submit(&Register::role( + // Temporarily grant a multisig role to the trigger authority to delegate the role to the signatories + Role::new(role_id.clone(), context.authority.clone()), + )) + .dbg_expect("accounts registry should successfully register a multisig role"); + + for signatory in args.signatories.keys().cloned() { + let is_multisig_again = { + let sub_role_id: RoleId = format!( + "multisig_signatory_{}_{}", + signatory.signatory(), + signatory.domain() + ) + .parse() + .dbg_unwrap(); + + host.query(FindRoleIds) + .filter_with(|role_id| role_id.eq(sub_role_id)) + .execute_single_opt() + .dbg_unwrap() + .is_some() + }; + + if is_multisig_again { + // Allow the transactions registry to write to the sub registry + let sub_registry_id: TriggerId = format!( + "multisig_transactions_{}_{}", + signatory.signatory(), + signatory.domain() + ) + .parse() + .dbg_unwrap(); + + host.submit(&Grant::account_permission( + CanExecuteTrigger { + trigger: sub_registry_id, + }, + account_id.clone(), + )) + .dbg_expect( + "accounts registry should successfully grant permission to the multisig account", + ); + } + + host.submit(&Grant::account_role(role_id.clone(), signatory)) + .dbg_expect( + "accounts registry should successfully grant the multisig role to signatories", + ); + } + + host.submit(&Revoke::account_role(role_id.clone(), context.authority)) + .dbg_expect( + "accounts registry should successfully revoke the multisig role from the trigger authority", + ); +} diff --git a/wasm_samples/multisig/Cargo.toml b/wasm/libs/multisig_domains/Cargo.toml similarity index 84% rename from wasm_samples/multisig/Cargo.toml rename to wasm/libs/multisig_domains/Cargo.toml index 4c53ff63ae7..ae37e394289 100644 --- a/wasm_samples/multisig/Cargo.toml +++ b/wasm/libs/multisig_domains/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "multisig" +name = "multisig_domains" edition.workspace = true version.workspace = true @@ -12,7 +12,7 @@ crate-type = ['cdylib'] [dependencies] iroha_trigger.workspace = true -executor_custom_data_model.workspace = true +iroha_executor_data_model.workspace = true panic-halt.workspace = true dlmalloc.workspace = true diff --git a/wasm/libs/multisig_domains/src/lib.rs b/wasm/libs/multisig_domains/src/lib.rs new file mode 100644 index 00000000000..c60e3a9e802 --- /dev/null +++ b/wasm/libs/multisig_domains/src/lib.rs @@ -0,0 +1,82 @@ +//! Trigger of world-level authority to enable multisig functionality for domains + +#![no_std] + +extern crate alloc; +#[cfg(not(test))] +extern crate panic_halt; + +use alloc::format; + +use dlmalloc::GlobalDlmalloc; +use iroha_trigger::{ + debug::{dbg_panic, DebugExpectExt as _}, + prelude::*, +}; + +#[global_allocator] +static ALLOC: GlobalDlmalloc = GlobalDlmalloc; + +getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); + +// Binary containing common logic to each domain for handling multisig accounts +const MULTISIG_ACCOUNTS_WASM: &[u8] = core::include_bytes!(concat!( + core::env!("CARGO_MANIFEST_DIR"), + "/../../target/prebuilt/libs/multisig_accounts.wasm" +)); + +#[iroha_trigger::main] +fn main(host: Iroha, context: Context) { + let EventBox::Data(DataEvent::Domain(event)) = context.event else { + dbg_panic("trigger misused: must be triggered only by a domain event"); + }; + let (domain_id, domain_owner, owner_changed) = match event { + DomainEvent::Created(domain) => (domain.id().clone(), domain.owned_by().clone(), false), + DomainEvent::OwnerChanged(owner_changed) => ( + owner_changed.domain().clone(), + owner_changed.new_owner().clone(), + true, + ), + _ => dbg_panic( + "trigger misused: must be triggered only when domain created or owner changed", + ), + }; + + let accounts_registry_id: TriggerId = format!("multisig_accounts_{}", domain_id) + .parse() + .dbg_unwrap(); + + let accounts_registry = if owner_changed { + let existing = host + .query(FindTriggers::new()) + .filter_with(|trigger| trigger.id.eq(accounts_registry_id.clone())) + .execute_single() + .dbg_expect("accounts registry should be existing"); + + host.submit(&Unregister::trigger(existing.id().clone())) + .dbg_expect("accounts registry should be successfully unregistered"); + + Trigger::new( + existing.id().clone(), + Action::new( + existing.action().executable().clone(), + existing.action().repeats().clone(), + domain_owner, + existing.action().filter().clone(), + ), + ) + } else { + Trigger::new( + accounts_registry_id.clone(), + Action::new( + WasmSmartContract::from_compiled(MULTISIG_ACCOUNTS_WASM.to_vec()), + Repeats::Indefinitely, + domain_owner, + ExecuteTriggerEventFilter::new().for_trigger(accounts_registry_id.clone()), + ), + ) + }; + + host.submit(&Register::trigger(accounts_registry)) + .dbg_expect("accounts registry should be successfully registered"); +} diff --git a/wasm/libs/multisig_transactions/Cargo.toml b/wasm/libs/multisig_transactions/Cargo.toml new file mode 100644 index 00000000000..ab208ce239b --- /dev/null +++ b/wasm/libs/multisig_transactions/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "multisig_transactions" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[lib] +crate-type = ['cdylib'] + +[dependencies] +iroha_trigger.workspace = true +iroha_multisig_data_model.workspace = true + +panic-halt.workspace = true +dlmalloc.workspace = true +getrandom.workspace = true + +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, default-features = false } diff --git a/wasm/libs/multisig_transactions/src/lib.rs b/wasm/libs/multisig_transactions/src/lib.rs new file mode 100644 index 00000000000..f4ac819cce4 --- /dev/null +++ b/wasm/libs/multisig_transactions/src/lib.rs @@ -0,0 +1,233 @@ +//! Trigger given per multi-signature account to control multi-signature transactions + +#![no_std] + +extern crate alloc; +#[cfg(not(test))] +extern crate panic_halt; + +use alloc::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + format, + vec::Vec, +}; + +use dlmalloc::GlobalDlmalloc; +use iroha_multisig_data_model::MultisigTransactionArgs; +use iroha_trigger::{ + debug::{dbg_panic, DebugExpectExt as _}, + prelude::*, +}; + +#[global_allocator] +static ALLOC: GlobalDlmalloc = GlobalDlmalloc; + +getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); + +#[iroha_trigger::main] +fn main(host: Iroha, context: Context) { + let EventBox::ExecuteTrigger(event) = context.event else { + dbg_panic("trigger misused: must be triggered only by a call"); + }; + let trigger_id = context.id; + let args: MultisigTransactionArgs = event + .args() + .try_into_any() + .dbg_expect("args should be for a multisig transaction"); + let signatory = event.authority().clone(); + + let instructions_hash = match &args { + MultisigTransactionArgs::Propose(instructions) => HashOf::new(instructions), + MultisigTransactionArgs::Approve(instructions_hash) => *instructions_hash, + }; + let instructions_metadata_key: Name = format!("proposals/{instructions_hash}/instructions") + .parse() + .unwrap(); + let proposed_at_ms_metadata_key: Name = format!("proposals/{instructions_hash}/proposed_at_ms") + .parse() + .unwrap(); + let approvals_metadata_key: Name = format!("proposals/{instructions_hash}/approvals") + .parse() + .unwrap(); + + let signatories: BTreeMap = host + .query_single(FindTriggerMetadata::new( + trigger_id.clone(), + "signatories".parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + + // Recursively deploy multisig authentication down to the personal leaf signatories + for account_id in signatories.keys() { + let sub_transactions_registry_id: TriggerId = format!( + "multisig_transactions_{}_{}", + account_id.signatory(), + account_id.domain() + ) + .parse() + .unwrap(); + + if let Ok(_sub_registry) = host + .query(FindTriggers::new()) + .filter_with(|trigger| trigger.id.eq(sub_transactions_registry_id.clone())) + .execute_single() + { + let propose_to_approve_me: InstructionBox = { + let approve_me: InstructionBox = { + let args = MultisigTransactionArgs::Approve(instructions_hash); + ExecuteTrigger::new(trigger_id.clone()) + .with_args(&args) + .into() + }; + let args = MultisigTransactionArgs::Propose([approve_me].to_vec()); + + ExecuteTrigger::new(sub_transactions_registry_id.clone()) + .with_args(&args) + .into() + }; + host.submit(&propose_to_approve_me) + .dbg_expect("should successfully write to sub registry"); + } + } + + let mut block_headers = host.query(FindBlockHeaders).execute().dbg_unwrap(); + let now_ms: u64 = block_headers + .next() + .dbg_unwrap() + .dbg_unwrap() + .creation_time() + .as_millis() + .try_into() + .dbg_unwrap(); + + let (approvals, instructions) = match args { + MultisigTransactionArgs::Propose(instructions) => { + host.query_single(FindTriggerMetadata::new( + trigger_id.clone(), + approvals_metadata_key.clone(), + )) + .expect_err("instructions shouldn't already be proposed"); + + let approvals = BTreeSet::from([signatory.clone()]); + + host.submit(&SetKeyValue::trigger( + trigger_id.clone(), + instructions_metadata_key.clone(), + Json::new(&instructions), + )) + .dbg_unwrap(); + + host.submit(&SetKeyValue::trigger( + trigger_id.clone(), + proposed_at_ms_metadata_key.clone(), + Json::new(&now_ms), + )) + .dbg_unwrap(); + + host.submit(&SetKeyValue::trigger( + trigger_id.clone(), + approvals_metadata_key.clone(), + Json::new(&approvals), + )) + .dbg_unwrap(); + + (approvals, instructions) + } + MultisigTransactionArgs::Approve(_instructions_hash) => { + let mut approvals: BTreeSet = host + .query_single(FindTriggerMetadata::new( + trigger_id.clone(), + approvals_metadata_key.clone(), + )) + .dbg_expect("instructions should be proposed first") + .try_into_any() + .dbg_unwrap(); + + approvals.insert(signatory.clone()); + + host.submit(&SetKeyValue::trigger( + trigger_id.clone(), + approvals_metadata_key.clone(), + Json::new(&approvals), + )) + .dbg_unwrap(); + + let instructions: Vec = host + .query_single(FindTriggerMetadata::new( + trigger_id.clone(), + instructions_metadata_key.clone(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + + (approvals, instructions) + } + }; + + let quorum: u16 = host + .query_single(FindTriggerMetadata::new( + trigger_id.clone(), + "quorum".parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + + let is_authenticated = quorum + <= signatories + .into_iter() + .filter(|(id, _)| approvals.contains(&id)) + .map(|(_, weight)| weight as u16) + .sum(); + + let is_expired = { + let proposed_at_ms: u64 = host + .query_single(FindTriggerMetadata::new( + trigger_id.clone(), + proposed_at_ms_metadata_key.clone(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + + let transaction_ttl_ms: u64 = host + .query_single(FindTriggerMetadata::new( + trigger_id.clone(), + "transaction_ttl_ms".parse().unwrap(), + )) + .dbg_unwrap() + .try_into_any() + .dbg_unwrap(); + + proposed_at_ms.saturating_add(transaction_ttl_ms) < now_ms + }; + + if is_authenticated || is_expired { + // Cleanup approvals and instructions + host.submit(&RemoveKeyValue::trigger( + trigger_id.clone(), + approvals_metadata_key, + )) + .dbg_unwrap(); + host.submit(&RemoveKeyValue::trigger( + trigger_id.clone(), + proposed_at_ms_metadata_key, + )) + .dbg_unwrap(); + host.submit(&RemoveKeyValue::trigger( + trigger_id.clone(), + instructions_metadata_key, + )) + .dbg_unwrap(); + + if !is_expired { + // Execute instructions proposal which collected enough approvals + for isi in instructions { + host.submit(&isi).dbg_unwrap(); + } + } + } +} diff --git a/wasm_samples/create_nft_for_every_user_trigger/Cargo.toml b/wasm/samples/create_nft_for_every_user_trigger/Cargo.toml similarity index 100% rename from wasm_samples/create_nft_for_every_user_trigger/Cargo.toml rename to wasm/samples/create_nft_for_every_user_trigger/Cargo.toml diff --git a/wasm_samples/create_nft_for_every_user_trigger/src/lib.rs b/wasm/samples/create_nft_for_every_user_trigger/src/lib.rs similarity index 96% rename from wasm_samples/create_nft_for_every_user_trigger/src/lib.rs rename to wasm/samples/create_nft_for_every_user_trigger/src/lib.rs index 415329d8a0f..355261b4c7f 100644 --- a/wasm_samples/create_nft_for_every_user_trigger/src/lib.rs +++ b/wasm/samples/create_nft_for_every_user_trigger/src/lib.rs @@ -20,7 +20,8 @@ fn main(host: Iroha, _context: Context) { iroha_trigger::log::info!("Executing trigger"); let accounts_cursor = host.query(FindAccounts).execute().dbg_unwrap(); - let bad_domain_ids: [DomainId; 2] = [ + let bad_domain_ids: [DomainId; 3] = [ + "system".parse().dbg_unwrap(), "genesis".parse().dbg_unwrap(), "garden_of_live_flowers".parse().dbg_unwrap(), ]; diff --git a/wasm_samples/executor_custom_data_model/Cargo.toml b/wasm/samples/executor_custom_data_model/Cargo.toml similarity index 100% rename from wasm_samples/executor_custom_data_model/Cargo.toml rename to wasm/samples/executor_custom_data_model/Cargo.toml diff --git a/wasm_samples/executor_custom_data_model/src/complex_isi.rs b/wasm/samples/executor_custom_data_model/src/complex_isi.rs similarity index 99% rename from wasm_samples/executor_custom_data_model/src/complex_isi.rs rename to wasm/samples/executor_custom_data_model/src/complex_isi.rs index 262b875db35..f293c256f39 100644 --- a/wasm_samples/executor_custom_data_model/src/complex_isi.rs +++ b/wasm/samples/executor_custom_data_model/src/complex_isi.rs @@ -1,6 +1,6 @@ //! Example of custom expression system. //! Only few expressions are implemented to show proof-of-concept. -//! See `wasm_samples/executor_custom_instructions_complex`. +//! See `wasm/samples/executor_custom_instructions_complex`. //! This is simplified version of expression system from Iroha v2.0.0-pre-rc.20 pub use evaluate::*; diff --git a/wasm_samples/executor_custom_data_model/src/lib.rs b/wasm/samples/executor_custom_data_model/src/lib.rs similarity index 91% rename from wasm_samples/executor_custom_data_model/src/lib.rs rename to wasm/samples/executor_custom_data_model/src/lib.rs index 62cf2d4c6a2..ea920f09c12 100644 --- a/wasm_samples/executor_custom_data_model/src/lib.rs +++ b/wasm/samples/executor_custom_data_model/src/lib.rs @@ -6,7 +6,6 @@ extern crate alloc; pub mod complex_isi; pub mod mint_rose_args; -pub mod multisig; pub mod parameters; pub mod permissions; pub mod simple_isi; diff --git a/wasm_samples/executor_custom_data_model/src/mint_rose_args.rs b/wasm/samples/executor_custom_data_model/src/mint_rose_args.rs similarity index 100% rename from wasm_samples/executor_custom_data_model/src/mint_rose_args.rs rename to wasm/samples/executor_custom_data_model/src/mint_rose_args.rs diff --git a/wasm_samples/executor_custom_data_model/src/parameters.rs b/wasm/samples/executor_custom_data_model/src/parameters.rs similarity index 100% rename from wasm_samples/executor_custom_data_model/src/parameters.rs rename to wasm/samples/executor_custom_data_model/src/parameters.rs diff --git a/wasm_samples/executor_custom_data_model/src/permissions.rs b/wasm/samples/executor_custom_data_model/src/permissions.rs similarity index 100% rename from wasm_samples/executor_custom_data_model/src/permissions.rs rename to wasm/samples/executor_custom_data_model/src/permissions.rs diff --git a/wasm_samples/executor_custom_data_model/src/simple_isi.rs b/wasm/samples/executor_custom_data_model/src/simple_isi.rs similarity index 96% rename from wasm_samples/executor_custom_data_model/src/simple_isi.rs rename to wasm/samples/executor_custom_data_model/src/simple_isi.rs index 9b11a35e29c..6094d8e7e61 100644 --- a/wasm_samples/executor_custom_data_model/src/simple_isi.rs +++ b/wasm/samples/executor_custom_data_model/src/simple_isi.rs @@ -1,5 +1,5 @@ //! Example of one custom instruction. -//! See `wasm_samples/executor_custom_instructions_simple`. +//! See `wasm/samples/executor_custom_instructions_simple`. use alloc::{format, string::String, vec::Vec}; diff --git a/wasm_samples/executor_custom_instructions_complex/Cargo.toml b/wasm/samples/executor_custom_instructions_complex/Cargo.toml similarity index 100% rename from wasm_samples/executor_custom_instructions_complex/Cargo.toml rename to wasm/samples/executor_custom_instructions_complex/Cargo.toml diff --git a/wasm_samples/executor_custom_instructions_complex/src/lib.rs b/wasm/samples/executor_custom_instructions_complex/src/lib.rs similarity index 100% rename from wasm_samples/executor_custom_instructions_complex/src/lib.rs rename to wasm/samples/executor_custom_instructions_complex/src/lib.rs diff --git a/wasm_samples/executor_custom_instructions_simple/Cargo.toml b/wasm/samples/executor_custom_instructions_simple/Cargo.toml similarity index 100% rename from wasm_samples/executor_custom_instructions_simple/Cargo.toml rename to wasm/samples/executor_custom_instructions_simple/Cargo.toml diff --git a/wasm_samples/executor_custom_instructions_simple/src/lib.rs b/wasm/samples/executor_custom_instructions_simple/src/lib.rs similarity index 100% rename from wasm_samples/executor_custom_instructions_simple/src/lib.rs rename to wasm/samples/executor_custom_instructions_simple/src/lib.rs diff --git a/wasm_samples/executor_remove_permission/Cargo.toml b/wasm/samples/executor_remove_permission/Cargo.toml similarity index 100% rename from wasm_samples/executor_remove_permission/Cargo.toml rename to wasm/samples/executor_remove_permission/Cargo.toml diff --git a/wasm_samples/executor_remove_permission/src/lib.rs b/wasm/samples/executor_remove_permission/src/lib.rs similarity index 100% rename from wasm_samples/executor_remove_permission/src/lib.rs rename to wasm/samples/executor_remove_permission/src/lib.rs diff --git a/wasm_samples/executor_with_admin/Cargo.toml b/wasm/samples/executor_with_admin/Cargo.toml similarity index 100% rename from wasm_samples/executor_with_admin/Cargo.toml rename to wasm/samples/executor_with_admin/Cargo.toml diff --git a/wasm_samples/executor_with_admin/src/lib.rs b/wasm/samples/executor_with_admin/src/lib.rs similarity index 100% rename from wasm_samples/executor_with_admin/src/lib.rs rename to wasm/samples/executor_with_admin/src/lib.rs diff --git a/wasm_samples/executor_with_custom_parameter/Cargo.toml b/wasm/samples/executor_with_custom_parameter/Cargo.toml similarity index 100% rename from wasm_samples/executor_with_custom_parameter/Cargo.toml rename to wasm/samples/executor_with_custom_parameter/Cargo.toml diff --git a/wasm_samples/executor_with_custom_parameter/src/lib.rs b/wasm/samples/executor_with_custom_parameter/src/lib.rs similarity index 100% rename from wasm_samples/executor_with_custom_parameter/src/lib.rs rename to wasm/samples/executor_with_custom_parameter/src/lib.rs diff --git a/wasm_samples/executor_with_custom_permission/Cargo.toml b/wasm/samples/executor_with_custom_permission/Cargo.toml similarity index 100% rename from wasm_samples/executor_with_custom_permission/Cargo.toml rename to wasm/samples/executor_with_custom_permission/Cargo.toml diff --git a/wasm_samples/executor_with_custom_permission/src/lib.rs b/wasm/samples/executor_with_custom_permission/src/lib.rs similarity index 100% rename from wasm_samples/executor_with_custom_permission/src/lib.rs rename to wasm/samples/executor_with_custom_permission/src/lib.rs diff --git a/wasm_samples/executor_with_migration_fail/Cargo.toml b/wasm/samples/executor_with_migration_fail/Cargo.toml similarity index 100% rename from wasm_samples/executor_with_migration_fail/Cargo.toml rename to wasm/samples/executor_with_migration_fail/Cargo.toml diff --git a/wasm_samples/executor_with_migration_fail/src/lib.rs b/wasm/samples/executor_with_migration_fail/src/lib.rs similarity index 100% rename from wasm_samples/executor_with_migration_fail/src/lib.rs rename to wasm/samples/executor_with_migration_fail/src/lib.rs diff --git a/wasm_samples/mint_rose_trigger/Cargo.toml b/wasm/samples/mint_rose_trigger/Cargo.toml similarity index 100% rename from wasm_samples/mint_rose_trigger/Cargo.toml rename to wasm/samples/mint_rose_trigger/Cargo.toml diff --git a/wasm_samples/mint_rose_trigger/src/lib.rs b/wasm/samples/mint_rose_trigger/src/lib.rs similarity index 100% rename from wasm_samples/mint_rose_trigger/src/lib.rs rename to wasm/samples/mint_rose_trigger/src/lib.rs diff --git a/wasm_samples/mint_rose_trigger_args/Cargo.toml b/wasm/samples/mint_rose_trigger_args/Cargo.toml similarity index 100% rename from wasm_samples/mint_rose_trigger_args/Cargo.toml rename to wasm/samples/mint_rose_trigger_args/Cargo.toml diff --git a/wasm_samples/mint_rose_trigger_args/src/lib.rs b/wasm/samples/mint_rose_trigger_args/src/lib.rs similarity index 100% rename from wasm_samples/mint_rose_trigger_args/src/lib.rs rename to wasm/samples/mint_rose_trigger_args/src/lib.rs diff --git a/wasm_samples/query_assets_and_save_cursor/Cargo.toml b/wasm/samples/query_assets_and_save_cursor/Cargo.toml similarity index 100% rename from wasm_samples/query_assets_and_save_cursor/Cargo.toml rename to wasm/samples/query_assets_and_save_cursor/Cargo.toml diff --git a/wasm_samples/query_assets_and_save_cursor/src/lib.rs b/wasm/samples/query_assets_and_save_cursor/src/lib.rs similarity index 100% rename from wasm_samples/query_assets_and_save_cursor/src/lib.rs rename to wasm/samples/query_assets_and_save_cursor/src/lib.rs diff --git a/wasm_samples/smart_contract_can_filter_queries/Cargo.toml b/wasm/samples/smart_contract_can_filter_queries/Cargo.toml similarity index 100% rename from wasm_samples/smart_contract_can_filter_queries/Cargo.toml rename to wasm/samples/smart_contract_can_filter_queries/Cargo.toml diff --git a/wasm_samples/smart_contract_can_filter_queries/src/lib.rs b/wasm/samples/smart_contract_can_filter_queries/src/lib.rs similarity index 100% rename from wasm_samples/smart_contract_can_filter_queries/src/lib.rs rename to wasm/samples/smart_contract_can_filter_queries/src/lib.rs diff --git a/wasm_samples/README.md b/wasm_samples/README.md deleted file mode 100644 index daf131336ad..00000000000 --- a/wasm_samples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Smatcontracts - -This folder contains test smartcontracts that are used in tests. - -All this smartcontracts are being built **automatically** by the [`build-script`](../../../build.rs) which iterates over this directory. \ No newline at end of file diff --git a/wasm_samples/default_executor/.cargo/config.toml b/wasm_samples/default_executor/.cargo/config.toml deleted file mode 100644 index 435ed755ec2..00000000000 --- a/wasm_samples/default_executor/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/wasm_samples/executor_custom_data_model/src/multisig.rs b/wasm_samples/executor_custom_data_model/src/multisig.rs deleted file mode 100644 index 6cbe685e559..00000000000 --- a/wasm_samples/executor_custom_data_model/src/multisig.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Arguments to register and manage multisig account - -use alloc::{collections::btree_set::BTreeSet, vec::Vec}; - -use iroha_data_model::{account::NewAccount, prelude::*}; -use serde::{Deserialize, Serialize}; - -/// Arguments to multisig account register trigger -#[derive(Serialize, Deserialize)] -pub struct MultisigRegisterArgs { - // Account id of multisig account should be manually checked to not have corresponding private key (or having master key is ok) - pub account: NewAccount, - // List of accounts responsible for handling multisig account - pub signatories: BTreeSet, -} - -/// Arguments to multisig account manager trigger -#[derive(Serialize, Deserialize)] -pub enum MultisigArgs { - /// Accept instructions proposal and initialize votes with the proposer's one - Instructions(Vec), - /// Accept vote for certain instructions - Vote(HashOf>), -} - -impl From for Json { - fn from(details: MultisigRegisterArgs) -> Self { - Json::new(details) - } -} - -impl TryFrom<&Json> for MultisigRegisterArgs { - type Error = serde_json::Error; - - fn try_from(payload: &Json) -> serde_json::Result { - serde_json::from_str::(payload.as_ref()) - } -} - -impl From for Json { - fn from(details: MultisigArgs) -> Self { - Json::new(details) - } -} - -impl TryFrom<&Json> for MultisigArgs { - type Error = serde_json::Error; - - fn try_from(payload: &Json) -> serde_json::Result { - serde_json::from_str::(payload.as_ref()) - } -} diff --git a/wasm_samples/multisig/src/lib.rs b/wasm_samples/multisig/src/lib.rs deleted file mode 100644 index 8722bac4b6b..00000000000 --- a/wasm_samples/multisig/src/lib.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Trigger to control multisignature account - -#![no_std] - -extern crate alloc; -#[cfg(not(test))] -extern crate panic_halt; - -use alloc::{collections::btree_set::BTreeSet, format, vec::Vec}; - -use dlmalloc::GlobalDlmalloc; -use executor_custom_data_model::multisig::MultisigArgs; -use iroha_trigger::{ - debug::{dbg_panic, DebugExpectExt as _}, - prelude::*, -}; - -#[global_allocator] -static ALLOC: GlobalDlmalloc = GlobalDlmalloc; - -getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); - -#[iroha_trigger::main] -fn main(host: Iroha, context: Context) { - let trigger_id = context.id; - - let EventBox::ExecuteTrigger(event) = context.event else { - dbg_panic("only work as by call trigger"); - }; - - let args: MultisigArgs = event - .args() - .try_into_any() - .dbg_expect("failed to parse arguments"); - - let signatory = event.authority().clone(); - let instructions_hash = match &args { - MultisigArgs::Instructions(instructions) => HashOf::new(instructions), - MultisigArgs::Vote(instructions_hash) => *instructions_hash, - }; - let votes_metadata_key: Name = format!("{instructions_hash}/votes").parse().unwrap(); - let instructions_metadata_key: Name = - format!("{instructions_hash}/instructions").parse().unwrap(); - - let (votes, instructions) = match args { - MultisigArgs::Instructions(instructions) => { - host.query_single(FindTriggerMetadata::new( - trigger_id.clone(), - votes_metadata_key.clone(), - )) - .expect_err("instructions are already submitted"); - - let votes = BTreeSet::from([signatory.clone()]); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - instructions_metadata_key.clone(), - Json::new(&instructions), - )) - .dbg_unwrap(); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - votes_metadata_key.clone(), - Json::new(&votes), - )) - .dbg_unwrap(); - - (votes, instructions) - } - MultisigArgs::Vote(_instructions_hash) => { - let mut votes: BTreeSet = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - votes_metadata_key.clone(), - )) - .dbg_expect("instructions should be submitted first") - .try_into_any() - .dbg_unwrap(); - - votes.insert(signatory.clone()); - - host.submit(&SetKeyValue::trigger( - trigger_id.clone(), - votes_metadata_key.clone(), - Json::new(&votes), - )) - .dbg_unwrap(); - - let instructions: Vec = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - instructions_metadata_key.clone(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - (votes, instructions) - } - }; - - let signatories: BTreeSet = host - .query_single(FindTriggerMetadata::new( - trigger_id.clone(), - "signatories".parse().unwrap(), - )) - .dbg_unwrap() - .try_into_any() - .dbg_unwrap(); - - // Require N of N signatures - if votes.is_superset(&signatories) { - // Cleanup votes and instructions - host.submit(&RemoveKeyValue::trigger( - trigger_id.clone(), - votes_metadata_key, - )) - .dbg_unwrap(); - host.submit(&RemoveKeyValue::trigger( - trigger_id.clone(), - instructions_metadata_key, - )) - .dbg_unwrap(); - - // Execute instructions proposal which collected enough votes - for isi in &instructions { - host.submit(isi).dbg_unwrap(); - } - } -} diff --git a/wasm_samples/multisig_register/build.rs b/wasm_samples/multisig_register/build.rs deleted file mode 100644 index 2195810702a..00000000000 --- a/wasm_samples/multisig_register/build.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Compile trigger to handle multisig actions - -use std::{io::Write, path::Path}; - -const TRIGGER_DIR: &str = "../multisig"; - -fn main() -> Result<(), Box> { - println!("cargo::rerun-if-changed={}", TRIGGER_DIR); - - let out_dir = std::env::var("OUT_DIR").unwrap(); - let wasm = iroha_wasm_builder::Builder::new(TRIGGER_DIR) - .show_output() - .build()? - .optimize()? - .into_bytes()?; - - let mut file = std::fs::File::create(Path::new(&out_dir).join("multisig.wasm"))?; - file.write_all(&wasm)?; - Ok(()) -} diff --git a/wasm_samples/multisig_register/src/lib.rs b/wasm_samples/multisig_register/src/lib.rs deleted file mode 100644 index 821c22a33a0..00000000000 --- a/wasm_samples/multisig_register/src/lib.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Trigger which register multisignature account and create trigger to control it - -#![no_std] - -extern crate alloc; -#[cfg(not(test))] -extern crate panic_halt; - -use alloc::format; - -use dlmalloc::GlobalDlmalloc; -use executor_custom_data_model::multisig::MultisigRegisterArgs; -use iroha_executor_data_model::permission::trigger::CanExecuteTrigger; -use iroha_trigger::{ - debug::{dbg_panic, DebugExpectExt as _}, - prelude::*, -}; - -#[global_allocator] -static ALLOC: GlobalDlmalloc = GlobalDlmalloc; - -getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); - -// Trigger wasm code for handling multisig logic -const WASM: &[u8] = core::include_bytes!(concat!(core::env!("OUT_DIR"), "/multisig.wasm")); - -#[iroha_trigger::main] -fn main(host: Iroha, context: Context) { - let EventBox::ExecuteTrigger(event) = context.event else { - dbg_panic("Only work as by call trigger"); - }; - - let args: MultisigRegisterArgs = event - .args() - .try_into_any() - .dbg_expect("failed to parse args"); - - let account_id = args.account.id().clone(); - host.submit(&Register::account(args.account)) - .dbg_expect("failed to register multisig account"); - - let trigger_id: TriggerId = format!( - "{}_{}_multisig_trigger", - account_id.signatory(), - account_id.domain() - ) - .parse() - .dbg_expect("failed to parse trigger id"); - - let payload = WasmSmartContract::from_compiled(WASM.to_vec()); - let trigger = Trigger::new( - trigger_id.clone(), - Action::new( - payload, - Repeats::Indefinitely, - account_id.clone(), - ExecuteTriggerEventFilter::new().for_trigger(trigger_id.clone()), - ), - ); - - host.submit(&Register::trigger(trigger)) - .dbg_expect("failed to register multisig trigger"); - - let role_id: RoleId = format!( - "{}_{}_signatories", - account_id.signatory(), - account_id.domain() - ) - .parse() - .dbg_expect("failed to parse role"); - - let can_execute_multisig_trigger = CanExecuteTrigger { - trigger: trigger_id.clone(), - }; - - host.submit(&Register::role( - // FIX: args.account.id() should be used but I can't - // execute an instruction from a different account - Role::new(role_id.clone(), context.authority).add_permission(can_execute_multisig_trigger), - )) - .dbg_expect("failed to register multisig role"); - - host.submit(&SetKeyValue::trigger( - trigger_id, - "signatories".parse().unwrap(), - Json::new(&args.signatories), - )) - .dbg_unwrap(); - - for signatory in args.signatories { - host.submit(&Grant::account_role(role_id.clone(), signatory)) - .dbg_expect("failed to grant multisig role to account"); - } -}